# フィジカルAI研究室を作る Log#22 | mechArm 270 の AI の直しが、ゲームパッドで1回13秒に

- 記録: LOG450（フィジカルAI研究室）
- 公開: 2026/11/6
- 記事のページ: https://pixelstomotors.com/record/gamepad-fixes-mecharm-270-ai-in-13-seconds
- サイト: Pixels to Motors（ADAPT DESIGN / 島津 栄一） https://pixelstomotors.com/
- 要約: mechArm 270 を動かす Physical Intelligence の π0.5 の失敗を人がゲームパッドで直すと1回は中央13秒で、キーで直した前の回の41〜51秒より短い。実機9回で19回直し、直す所は毎回同じ2か所だった。

> AI が読むための記事の全文です（コード・実行ログを含む）。引用するときは上の「記事のページ」の住所を使ってください。

---

小型ロボットアーム mechArm 270 を Physical Intelligence の π0.5 で動かし、失敗しかけた所を人がゲームパッドで直すと、1回の直しにかかった時間は中央の値で約13秒でした。キーを1回ずつ打って直していた前の回（10月31日と11月1日）は41〜51秒だったので、およそ4分の1です。実物のアームで9回動かして19回直し、直した所は9回とも同じ2か所でした。

この回でやったことで、次の3つが変わりました。

- 良くなること：AI が失敗しかけたときの直しが、キーボードの1°ずつから、ゲームパッドの長押しになった。つかんだあとの持ち上げ（肩を約25°）も、十字キーを押し続けるだけで済む。19回の直しで、押したのに受け付けられなかった操作は0回だった
- もたらされること：人が直した成功の記録を、次の学習データにそのまま回せるようになった。この回の成功4本を足したデータで、新しいモデル（corr3）を一晩で学習させた。前の回（LOG446）で直したアームの中の受け口のおかげで、AI の1回分の動き（推論1回分）を実行する時間も 6.6秒から1.7秒に縮んだ
- 時間・費用：推論に借りた GPU は23分21秒で約36円、学習は約3.1時間で約292円。9回の直しを全部合わせて280秒（4分40秒）で、前の回のキーボードの直しは14回で657秒だった

π0.5（パイレイテンゴ）は、米国の [Physical Intelligence](https://www.pi.website/) 社が公開しているロボット用の AI のモデルです。上から見たカメラの画像と6つの関節の角度を受け取り、次の1秒ぶんの関節の角度を返します。人が手で教えた19回のお手本と、人が直した成功の記録で学習させ、赤いブロックをつかんで白い箱に入れる作業をさせています。AI が失敗しかけたら、その場で人が関節を直し、直した動きも学習に回す。これを繰り返して、人の直しが要らなくなるところまで持っていくのが、このシリーズのいまの狙いです。

**9回とも、直したのはつかむ前の根元の向き（約2°）と、つかんだあとの持ち上げ（肩を約25°）の2か所で、失敗した4回は、どれもつかんだあとにブロックが落ちていました。** 落ちた回の録画では、持ち上げたあとのブロックが爪の中で斜めに回っています。つかみが浅いのか、爪の締めが緩いのかは、この記録だけでは分かっていません。

[動画: 成功した1回目（18:39）の録画。つかんだあと、AI の持ち上げが足りない所でゲームパッドに切り替え、肩を長押しで持ち上げてから AI に戻し、箱の上で爪を開くまで。記録のコマを1秒10コマで並べたので、実際より少し速い](https://pixelstomotors.com/media/LOG450_01_success_lift_by_gamepad.mp4)

やったことの全体は次のとおりです。

| 段階 | やったこと | 結果 |
|---|---|---|
| 推論の補正をゲームパッドにつなぐ（11/5） | AI の推論のあとの確認で p を押すと、ゲームパッドの操作画面が開き、□ で推論に戻るようにした | アームなしの試しとテスト（新しく4件）で流れを確かめた |
| 実機で9回動かす（11/6 18:39〜18:58） | 借りた GPU（[Runpod](https://www.runpod.io/) の A40）の上のモデル corr2 で、mechArm 270 を動かした | 成功5回・失敗4回。19回の直しはすべてゲームパッド。受付なし0回 |
| 直しと動きの時間を数える | 9回の記録から、直しの中身と時間、推論1回分の実行時間を数えた | 直しは中央13.2秒（キーの回は41〜51秒）。推論1回分は1.72秒（前は6.6秒） |
| 次の学習データを作る（依頼97） | 成功5本の手首を点検し、外れのない4本を前の7本に足して corr_v5 を作った | 11本・1,584コマ。185554 は手首が指令なしに約6°動いた所があり外した |
| corr3 を学習する（19:51〜23:04） | corr2 から1,500ステップ続けて学習した | loss は 0.0129 から 0.0030 へ。Hugging Face と KIOXIA に置いて Pod を消した |

次は、corr3 で実機を動かし、つかんだあとに AI が自分で持ち上げるようになったか、根元の向きの直しがどこまで減るかを確かめます。

GPU を買うか借りるかの判断は、Runpod で実際に借りて学習させた費用をもとに、別のページにまとめています。

[ロボットAIの学習用GPU、買うか借りるか](https://pixelstomotors.com/gpu-buy-or-rent)

この回の作業は、Claude（手順と分析を一緒に組み立てた AI）と進めました。アームとゲームパッドの操作と、目で見た判断はこちらで行い、記録の集計と推論のプログラムの直しは Claude が行っています。次の学習データづくりと学習の準備は [Claude Code](https://claude.com/claude-code)（プログラムを書く AI）に依頼箱を通して頼んでいます。ここから下は、使ったコマンド・出力・依頼文・集計のコードを全量で載せた作業ログです。ログの中の Pod の IP アドレスとポート番号は `<POD_IP>` と `<SSH_PORT>` に置き換えて記録されています。鍵（Runpod と Hugging Face のアクセストークン）は、どこにも載せていません。費用の円は 1ドル＝158円 で計算しています。結果の数字は記録から数えた値で、見込みのものは見込みと書いています。


## 作業ログ ― ゲームパッドの補正を π0.5 の推論につなぎ、9回直して、直した記録で corr3 を学習するまで

### この回で使う言葉

| 言葉 | 意味 |
|---|---|
| 推論1回分（チャンク） | AI に1回問い合わせて返ってくる、次の1秒ぶん（10ステップ）の関節の角度。プログラムはこれを1つ受け取るたびに止まり、実行するかを聞く。記録の「推論3」は、0から数えて4つ目 |
| p（ゲームパッドで補正） | この回で足した操作。推論のあとの確認で p を押すと、ゲームパッドの操作画面が開く。直し終えて □ を押すと、直した姿勢から AI に戻る |
| c（キーで補正） | 前の回まで使っていた直し方。キーで関節を選び、1回押すごとに決まった角度だけ動かす |
| 1刻み・長押し | ゲームパッドの十字キーの2つの押し方。短く押して離すと1°だけ動く。0.4秒より長く押し続けると、15°先の目標へ送り、離したところで止める（LOG446 で決めた形） |
| j0〜j5 | mechArm 270 の6つの関節。j0 は根元の回転、j1 は肩、j2 はひじ、j3〜j5 は手首 |
| daemon（デーモン） | アームの中の小さな計算機（Raspberry Pi）で動き続け、Mac からの指令を受けて関節を動かすプログラム。推論もゲームパッドも、ここを通ってアームを動かす |
| corr2・corr3 | 人が直した成功の記録を足して学び直したモデルの呼び名。corr2 は前の回（LOG422 のあと）に作ったもので、この回の実機で使った。corr3 は、この回の記録を足して corr2 から続けて学習したもの |
| corr_v5 | corr3 の学習に足したデータ。人が直した成功の記録11本 |
| 学習データの範囲 | お手本19回で関節が通った角度の幅に、10°の余裕を足したもの。推論で送る目標は、この範囲の外へ出さない |


## 11/5 19:30 推論の補正をゲームパッドでできるようにした（run_policy_client.py に p を足した）

前の回（LOG446）で、ゲームパッドで mechArm 270 を手で動かす道具（`tools/gamepad_teleop.py`）を作りました。ただ、それは推論とは別に起動する道具で、AI の推論の途中で使う直し（`pi05/run_policy_client.py` の c）は、キーボードで1°ずつ打つ形のままでした。そこで、推論のあとの確認にゲームパッドの直しを足しました。

最初は、p を押したときに gamepad_teleop を別のプログラムとして起動し、直し終えたら閉じる形を考えました。gamepad_teleop をほとんど変えずに済むからです。ところが、アームの中の daemon のコードを読むと、つながりを1本ずつしか受けない作りでした（`listen(1)` で受け、1本のつながりが切れるまで次を受けない）。推論のプログラムがつながったままだと、別のプログラムのゲームパッドは待たされて動きません。推論の側を切ると、今度は記録係（上カメラと関節の角度を1秒10コマで残す部分）が角度を読めなくなり、直している間の動きが学習データに残りません。

そのため、gamepad_teleop の操作の部分を、推論と同じプログラムの中で動かす形にしました。daemon へのつながりと、送る順番をそろえる鍵は、推論・記録係・ゲームパッドで共有します。

- ねらい：AI が失敗しかけた所を、LOG446 で固めたゲームパッドの操作で直し、その動きをそのまま学習データに残す
- 変わったこと：推論のあとの確認に p を足した。p で gamepad_teleop と同じ画面が開き、操作・送り方・守り（関節の動かせる範囲）は LOG446 と同じ。□ で、直した姿勢から推論に戻る。直している間の記録は補正（correct）として残り、爪の指令は送った瞬間に記録に入る。押した操作は log.jsonl に1つずつ残る
- 見込める効果：キーボードで1°ずつ打っていた直しが短くなる見込み（測った結果は、このあとの直しの時間の節）

守りは gamepad_teleop と同じにしました（関節の物理的な可動域、手首 j4 の −90〜+60°、肩 j1 は 90.87° まで）。学習データの範囲より広いので、範囲の外で □ を押したときは、次の推論で端へ切られるか止まることを画面に出します。直した補正は、推論に戻ったあと b で巻き戻せます（送った目標を逆にたどる。爪を動かした補正は、キーの補正と同じく巻き戻さない）。

テストは、新しく `pi05/test_pad_correction.py` の4件と、`tools/test_gamepad_teleop.py` の1件（動いている間の □ を断る）を足しました。本物の pygame（ゲームパッドを読むライブラリ）の画面を画面なしで動かし、偽のコントローラーの押しを流して確かめています。前からあるテストも通りました（`pi05/test_rewind_correct.py` の録画ファイルを見る1件だけは、Claude の作業環境の Linux では変更の前から落ち、Mac では通ることを依頼97で確かめています）。この回のコードは、記事の終わりに全文を載せています。

#### commit cd2fb46 の差分（run_policy_client.py・gamepad_teleop.py・テスト）

```diff
commit cd2fb4602ad724108e7c3b4f9b118b862801012e
Author: Eiichi Shimazu <studio@adaptdesign.jp>
Date:   Thu Nov 5 10:42:07 2026 +0000

    run_policy_client: 推論のあとの確認に p (ゲームパッドで補正) を足す. gamepad_teleop を同じプロセスで動かし □ で推論に戻る (LOG450)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXVQCng1HbyW9QDciA6

diff --git a/pi05/run_policy_client.py b/pi05/run_policy_client.py
index 599b090..f233c69 100644
--- a/pi05/run_policy_client.py
+++ b/pi05/run_policy_client.py
@@ -61,6 +61,13 @@
     お手本と同じ形). 1 コマごとに状態 (model / rewind / correct / home / wait) を付ける.
     wait = 推論のあとの確認で入力を待っている間 (人が手でブロックを置き直すので学習データに使わない)
     h / q で終えるときに 結果 (y / n / Enter=記録しない) を聞き, {run_stem}.json に残す
+  - p = ゲームパッドで補正 (2026-11-05 LOG450). tools/gamepad_teleop.py と同じ操作・送り方 (1 関節だけ送る 1 刻み・
+    15° 先へ 1 回送る長押し・離したら stop) のウィンドウが開き, □ で推論に戻る. 推論と同じプロセスで動かし,
+    daemon のつながりと鍵を共有する (daemon は 1 本ずつしか受けない). 守りは gamepad_teleop と同じ
+    (物理可動域・J4_SAFE・j1 ≤ 90.87. 学習データの範囲ではない). 範囲の外で終えたら, 次の推論で止まりうると出す.
+    間の状態は correct (学習データに使う). 爪の指令は送る瞬間に記録へ入れる. 押した操作は log.jsonl に
+    {"type": "pad", ...} で 1 つずつ, 終わりに {"type": "correction", "action": "accept", "device": "gamepad"}.
+    b で巻き戻せる (送った目標を逆にたどる. 爪を動かした補正は今までどおり断る)
   - 推論のあとの確認 (2026-10-31 依頼 40): 実行するのは何も打たずに Enter を押したときだけ.
     知らない入力 (R・c] ・空白だけ など) は実行せずに聞き直す. r は補正の中と同じく爪を締め直す
     (閉じているときだけ. チャンクは実行しない. log.jsonl に type=confirm / action=regrip_tighten).
@@ -891,6 +898,111 @@ def run_correction(arm, keys, lo, hi, max_move, grip, corr_id, chunk, log_f, rec
             return res, grip
 
 
+class PadLog:
+    """ゲームパッドの補正 (p) の記録: gamepad_teleop の 1 行を log.jsonl に {"type": "pad", ...} で書き, 手元にも残す."""
+
+    def __init__(self, log_f, corr_id, chunk):
+        self.log_f, self.corr_id, self.chunk = log_f, corr_id, chunk
+        self.rows = []
+
+    def write(self, rec):
+        row = {"type": "pad", "correction_id": self.corr_id, "after_chunk": self.chunk}
+        row.update(rec)
+        self.rows.append(row)
+        self.log_f.write(json.dumps(row, ensure_ascii=False, default=float) + "\n")
+        self.log_f.flush()
+
+    def close(self):
+        pass
+
+
+def _load_gamepad_teleop():
+    """tools/gamepad_teleop.py を読む (p を押したときだけ. pygame を使うので). 読めなければ None."""
+    # gamepad_teleop は run_policy_client を import して値と送り口を使う. 別の写しを読まないよう, 今動いているこの
+    # モジュールを同じ名前で登録する (--grip-wait などで変えた待ちを補正でも使う)
+    sys.modules.setdefault("run_policy_client", sys.modules[__name__])
+    tools = str(_ROOT / "tools")
+    if tools not in sys.path:
+        sys.path.insert(0, tools)
+    try:
+        import gamepad_teleop as gt
+    except Exception as ex:
+        print(f"[PAD   ] gamepad_teleop を読めません ({type(ex).__name__}: {ex}). c (キー) で補正してください")
+        return None
+    return gt
+
+
+def run_pad_correction(arm, grip, corr_id, chunk, log_f, recorder, lo, hi):
+    """p = ゲームパッドで補正 (2026-11-05 LOG450). 戻り値は run_correction と同じ (結果 dict, 補正後のグリッパー状態).
+    結果の action はいつも accept (取り消しは, 推論に戻ってから b で巻き戻す)."""
+    gt = _load_gamepad_teleop()
+    if gt is None:
+        return None, grip
+    start_q = arm.read()
+    if start_q is None:
+        print("[PAD   ] 関節角を読めないので補正に入れません")
+        return None, grip
+    start_q = np.asarray(start_q, dtype=float)[:6].copy()
+    if arm.execute:
+        gt.load_daemon_modules()
+
+        class PadArm(gt.DaemonArm):
+            """爪の指令を送る瞬間に記録へ入れる (build_correction_episodes は指令の時刻で 0/1 を切り替える)."""
+
+            def gripper(self, state, gate):
+                def _send():
+                    if recorder is not None:
+                        recorder.gripper(state)
+                    if state == 1:
+                        daemon_set_gripper_value(GRIP_VALUE)
+                    else:
+                        daemon_set_gripper(0, wait=GRIP_OPEN_DAEMON_WAIT)
+                sent = gate.run(_send)
+                if sent:
+                    time.sleep(GRIPPER_SLEEP)
+                return sent
+        pad_arm = PadArm()
+    else:
+        pad_arm = gt.DryArm(start_q)
+    plog = PadLog(log_f, corr_id, chunk)
+    if recorder is not None:
+        recorder.set_mode(MODE_CORRECT)
+    t_start = time.time()
+    print(f"[PAD   ] 補正 {corr_id} (推論 {chunk} のあと). ゲームパッドで直して, □ で推論に戻ります")
+    title = f"PAD CORRECTION {corr_id}  [{'EXECUTE' if arm.execute else 'DRY'}]  square: back to policy"
+    end_reason, tp, worker_done = gt.run_embedded(pad_arm, start_q, plog, grip=grip, mode_text=title)
+    t_end = time.time()
+    if recorder is not None:
+        recorder.set_mode(MODE_MODEL)
+    if not arm.execute:
+        arm.sim_q = np.asarray(pad_arm.q, dtype=float).copy()
+    sent = [np.asarray(r["command"], dtype=float) for r in plog.rows
+            if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None]
+    grip_rows = [r for r in plog.rows if r.get("kind") in ("grip_close", "grip_open") and r.get("sent") is True]
+    new_grip = int(grip_rows[-1]["grip_cmd"]) if grip_rows else grip
+    qa = arm.read()
+    res = {"type": "correction", "action": "accept", "device": "gamepad", "correction_id": corr_id,
+           "after_chunk": chunk, "t_start": t_start, "t_end": t_end, "duration_s": round(t_end - t_start, 2),
+           "start_pose": _fmt6(start_q), "final_target": _fmt6(sent[-1]) if sent else _fmt6(start_q),
+           "measured_after": _fmt6(qa), "steps_sent": len(sent), "pad_rows": len(plog.rows),
+           "grip_before": grip, "grip_after": new_grip, "grip_used": bool(grip_rows), "regrip_used": False,
+           "sent_targets": [_fmt6(x) for x in sent], "pad_end_reason": end_reason,
+           "pad_stopped": bool(tp.stopped), "pad_stop_reason": tp.stop_reason, "pad_worker_done": bool(worker_done),
+           "pad_ignored": sum(1 for r in plog.rows if r.get("accepted") is False)}
+    print(f"[PAD   ] 補正 {corr_id} を終えました ({t_end - t_start:.1f} 秒, 送った目標 {len(sent)} 回"
+          + (", 爪を動かした" if grip_rows else "") + "). この姿勢から推論に戻ります")
+    if qa is not None:
+        out = [k for k in range(6) if not (lo[k] - 0.5 <= qa[k] <= hi[k] + 0.5)]
+        if out:
+            res["outside_envelope"] = out
+            print("[PAD   ] [注意] 学習データの範囲の外にいます: "
+                  + ", ".join(f"j{k}={qa[k]:+.1f} (範囲 {lo[k]:+.0f}〜{hi[k]:+.0f})" for k in out)
+                  + ". 次の推論は範囲の端へ切られるか, 止まります")
+    if end_reason == "ctrl_c":
+        res["ctrl_c"] = True
+    return res, new_grip
+
+
 def write_trial_json(run_dir, run_stem, args, recorded, end_reason, outcome, history_log, n_rewinds,
                      n_corrections, exit_code):
     """{run_stem}.json. お手本の試行 JSON と同じキー (recording_enabled / result / judgments) を持つ.
@@ -928,9 +1040,9 @@ def flush_stdin():
         print(f"       [WARN] たまった入力を捨てられませんでした ({ex})")
 
 
-CONFIRM_PROMPT = ("       Enter=実行 / s=送らず次へ / b=巻き戻し / c=補正 / r=爪の締め直し / "
-                  "h=HOMEに戻して終了 / q=終了 > ")
-CONFIRM_KEYS = ("s", "b", "c", "r", "h", "q")
+CONFIRM_PROMPT = ("       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / "
+                  "r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > ")
+CONFIRM_KEYS = ("s", "b", "c", "p", "r", "h", "q")
 
 
 def ask_outcome():
@@ -1352,13 +1464,17 @@ def main():
                             if recorder is not None:
                                 recorder.set_mode(MODE_MODEL)
                         continue
-                    if ans == "c":
+                    if ans in ("c", "p"):
                         rec["correction_requested"] = True
+                        rec["correction_device"] = "gamepad" if ans == "p" else "keys"
                         log_f.write(json.dumps(rec) + "\n"); log_f.flush()
                         n_corrections += 1
-                        with KeyReader() as keys:
-                            cr, grip = run_correction(arm, keys, lo, hi, args.max_move_deg, grip,
-                                                      n_corrections, i, log_f, recorder)
+                        if ans == "p":
+                            cr, grip = run_pad_correction(arm, grip, n_corrections, i, log_f, recorder, lo, hi)
+                        else:
+                            with KeyReader() as keys:
+                                cr, grip = run_correction(arm, keys, lo, hi, args.max_move_deg, grip,
+                                                          n_corrections, i, log_f, recorder)
                         if cr is not None:
                             log_f.write(json.dumps(cr) + "\n"); log_f.flush()
                             if cr["grip_used"] or cr.get("regrip_used"):
@@ -1379,6 +1495,8 @@ def main():
                                                 "grip_changed": cr["grip_used"],
                                                 "regrip": cr["regrip_used"],
                                                 "t_start": cr["t_start"], "t_end": cr["t_end"]})
+                            if cr.get("ctrl_c"):
+                                raise KeyboardInterrupt
                         continue
             if not unsafe and args.dry_keys:
                 # --dry-keys の Enter: 送らずに仮の姿勢を目標へ移す (b / c の流れを試すため)
diff --git a/pi05/test_pad_correction.py b/pi05/test_pad_correction.py
new file mode 100644
index 0000000..54b132a
--- /dev/null
+++ b/pi05/test_pad_correction.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+"""test_pad_correction.py — run_policy_client.py の p (ゲームパッドで補正. 2026-11-05 LOG450) の確かめ.
+
+本物の pygame のループ (SDL_VIDEODRIVER=dummy) に偽のコントローラーの押しを流し, 補正の結果の記録・
+爪の記録・巻き戻し用の目標・ドライランの仮の姿勢・実機の送り口 (daemon を偽物にする) を見る.
+  SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python3 pi05/test_pad_correction.py
+"""
+import functools
+import io
+import json
+import os
+import sys
+import unittest
+from pathlib import Path
+from unittest import mock
+
+os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
+os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
+_HERE = Path(__file__).resolve().parent
+sys.path.insert(0, str(_HERE))
+sys.path.insert(0, str(_HERE.parent / "tools"))
+
+import numpy as np  # noqa: E402
+
+import run_policy_client as rpc  # noqa: E402
+import gamepad_teleop as gt  # noqa: E402
+
+
+class FakeJoystick:
+    """test_gamepad_teleop.FakeJoystick と同じ (あちらは読み込むと刻みを 0.5° に書き換えるので写した)."""
+
+    def __init__(self, idx):
+        self.axes = [0.0, 0.0, 0.0, 0.0, -1.0, -1.0]
+
+    def get_instance_id(self):
+        return 7
+
+    def get_name(self):
+        return "PS4 Controller"
+
+    def get_guid(self):
+        return "fake-guid"
+
+    def get_numaxes(self):
+        return 6
+
+    def get_numbuttons(self):
+        return 16
+
+    def get_numhats(self):
+        return 0
+
+    def get_axis(self, a):
+        return self.axes[a]
+
+    def get_button(self, b):
+        return 0
+
+HOME = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]
+
+
+class FakeRecorder:
+    def __init__(self):
+        self.events = []
+
+    def set_mode(self, m):
+        self.events.append(("mode", m))
+
+    def gripper(self, s):
+        self.events.append(("gripper", s))
+
+
+def script_frames(script):
+    import pygame
+
+    def btn(b, down=True):
+        t = pygame.JOYBUTTONDOWN if down else pygame.JOYBUTTONUP
+        pygame.event.post(pygame.event.Event(t, instance_id=7, button=b, joy=0))
+
+    def on_frame(n, tp):
+        if n == 1:
+            pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0))
+        for b in script.get(n, ()):
+            btn(b); btn(b, False)
+        if n == 600:
+            pygame.event.post(pygame.event.Event(pygame.QUIT))
+    return on_frame
+
+
+LO = np.array([-60.0, -10, -10, -60, -60, -60])
+HI = np.array([60.0, 60, 60, 60, 60, 0])
+
+
+class TestPadDry(unittest.TestCase):
+    def test_dry_flow(self):
+        arm = rpc.ArmIO(False, None, sim_q=HOME)
+        rec = FakeRecorder()
+        log_f = io.StringIO()
+        on_frame = script_frames({3: [gt.BTN_RIGHT], 40: [gt.BTN_RIGHT], 80: [gt.BTN_CROSS], 140: [gt.BTN_SQUARE]})
+        orig = gt.run_embedded
+        with mock.patch.object(gt, "run_embedded", functools.partial(orig, on_frame=on_frame)), \
+                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
+                mock.patch.object(gt, "GRIPPER_SLEEP", 0.0):
+            res, grip = rpc.run_pad_correction(arm, 0, 1, 4, log_f, rec, LO, HI)
+        self.assertEqual(res["action"], "accept")
+        self.assertEqual(res["device"], "gamepad")
+        self.assertEqual(res["pad_end_reason"], "finish_button")
+        self.assertEqual(res["steps_sent"], 2)
+        self.assertEqual(res["sent_targets"][-1][0], 2.0)
+        self.assertTrue(res["grip_used"])
+        self.assertEqual((res["grip_before"], res["grip_after"], grip), (0, 1, 1))
+        self.assertAlmostEqual(arm.sim_q[0], 2.0)                    # 推論に戻る姿勢 = 直した姿勢
+        self.assertEqual(rec.events[0], ("mode", rpc.MODE_CORRECT))
+        self.assertEqual(rec.events[-1], ("mode", rpc.MODE_MODEL))
+        rows = [json.loads(x) for x in log_f.getvalue().splitlines()]
+        self.assertTrue(rows and all(r["type"] == "pad" and r["correction_id"] == 1 and r["after_chunk"] == 4
+                                     for r in rows))
+        self.assertGreaterEqual(res["duration_s"], 0.0)
+        self.assertNotIn("outside_envelope", res)
+
+    def test_outside_envelope_warns(self):
+        arm = rpc.ArmIO(False, None, sim_q=[59.5, 0, 0, 0, 0, -30])
+        on_frame = script_frames({3: [gt.BTN_RIGHT], 60: [gt.BTN_RIGHT], 120: [gt.BTN_SQUARE]})
+        orig = gt.run_embedded
+        out = []
+        with mock.patch.object(gt, "run_embedded", functools.partial(orig, on_frame=on_frame)), \
+                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
+                mock.patch("builtins.print", lambda *a, **k: out.append(" ".join(map(str, a)))):
+            res, _ = rpc.run_pad_correction(arm, 0, 2, 0, io.StringIO(), None, LO, HI)
+        self.assertEqual(res["outside_envelope"], [0])
+        self.assertTrue(any("学習データの範囲の外" in x for x in out))
+
+
+class TestPadExecute(unittest.TestCase):
+    """実機の送り口: daemon を偽物にして, 1 関節の send_angle と, 爪の指令の瞬間の記録を見る."""
+
+    def test_execute_sends_and_records_gripper(self):
+        q = {"now": np.array(HOME, dtype=float)}
+        calls = []
+
+        def req(c, **k):
+            calls.append(c)
+            if c["cmd"] == "send_angle":
+                q["now"][c["joint_id"] - 1] = c["angle"]
+                return {"ok": True}
+            if c["cmd"] == "get_angles":
+                return {"ok": True, "angles": list(q["now"])}
+            return {"ok": True, "result": None}
+
+        rec = FakeRecorder()
+        arm = rpc.ArmIO(True, rec)
+        on_frame = script_frames({3: [gt.BTN_LEFT], 60: [gt.BTN_CROSS], 120: [gt.BTN_SQUARE]})
+        orig = gt.run_embedded
+        gt.load_daemon_modules()
+        self.assertIs(gt.rpc, sys.modules["run_policy_client"])
+        with mock.patch.object(gt, "run_embedded", functools.partial(orig, on_frame=on_frame)), \
+                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
+                mock.patch.object(rpc, "read_angles", lambda: q["now"].copy()), \
+                mock.patch.object(gt, "daemon_request", req), \
+                mock.patch.object(rpc, "daemon_set_gripper_value", lambda v: calls.append({"cmd": "grip", "v": v})), \
+                mock.patch.object(rpc, "GRIPPER_SLEEP", 0.0):
+            res, grip = rpc.run_pad_correction(arm, 0, 3, 7, io.StringIO(), rec, LO, HI)
+        self.assertEqual(res["pad_end_reason"], "finish_button")
+        self.assertIn({"cmd": "send_angle", "joint_id": 1, "angle": -1.0, "speed": gt.SPEED, "wait": rpc.SEND_WAIT},
+                      calls)
+        self.assertIn({"cmd": "grip", "v": rpc.GRIP_VALUE}, calls)
+        self.assertIn(("gripper", 1), rec.events)
+        self.assertEqual(grip, 1)
+        self.assertEqual(res["sent_targets"], [[-1.0, 0.0, 0.0, 0.0, 0.0, -30.0]])
+
+
+class TestConfirmP(unittest.TestCase):
+    """確認で p を押すと補正に入り, 終えたあとの b でその補正を巻き戻せる (アームなし --dry-keys)."""
+
+    def test_p_then_b(self):
+        import tempfile
+        import time
+        from test_rewind_correct import run_client
+
+        def fake_pad(arm, grip, corr_id, chunk, log_f, recorder, lo, hi):
+            start = arm.read()[:6].copy()
+            t0 = time.time()
+            tgt = start.copy(); tgt[0] += 3.0
+            arm.sim_q = tgt.copy()
+            return ({"type": "correction", "action": "accept", "device": "gamepad", "correction_id": corr_id,
+                     "after_chunk": chunk, "t_start": t0, "t_end": time.time(), "start_pose": rpc._fmt6(start),
+                     "final_target": rpc._fmt6(tgt), "measured_after": rpc._fmt6(tgt), "steps_sent": 1,
+                     "grip_before": grip, "grip_after": grip, "grip_used": False, "regrip_used": False,
+                     "sent_targets": [rpc._fmt6(tgt)]}, grip)
+        td = Path(tempfile.mkdtemp())
+        with mock.patch.object(rpc, "run_pad_correction", fake_pad):
+            code, world, run, lines, out = run_client(
+                rpc, ["--dry-keys", "--steps-per-chunk", "10", "--max-chunks", "4"], ["", "p", "b", "q"], td)
+        self.assertEqual(code, 0)
+        req = [x for x in lines if x.get("correction_requested")]
+        self.assertEqual([x["correction_device"] for x in req], ["gamepad"])
+        rw = [x for x in lines if x.get("type") == "rewind"]
+        self.assertEqual([(x["kind"], x["correction_id"]) for x in rw], [("correction", 1)])
+        self.assertIn("p=ゲームパッドで補正", rpc.CONFIRM_PROMPT)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tools/gamepad_teleop.py b/tools/gamepad_teleop.py
index e987267..158614f 100644
--- a/tools/gamepad_teleop.py
+++ b/tools/gamepad_teleop.py
@@ -1386,8 +1386,11 @@ def controller_info(js):
             "connected_at": datetime.now().isoformat(timespec="seconds")}
 
 
-def run_loop(tp, screen, fonts, mode_text, on_controller=None, on_frame=None):
-    """ウィンドウを閉じるまで回す. 戻り値: 終わった理由."""
+def run_loop(tp, screen, fonts, mode_text, on_controller=None, on_frame=None, finish_buttons=None):
+    """ウィンドウを閉じるまで回す. 戻り値: 終わった理由.
+    finish_buttons (run_policy_client の補正 p から. 2026-11-05 LOG450): このボタンを押したら, 止まっていれば
+    "finish_button" で戻る (推論に戻る). 動いている間 (送っている・長押し・止める待ち) は断って続ける.
+    このボタンは Teleop には渡さない."""
     js = None
     clock = pygame.time.Clock()
     frame = 0
@@ -1425,6 +1428,14 @@ def run_loop(tp, screen, fonts, mode_text, on_controller=None, on_frame=None):
             except Exception as ex:          # pygame.error など
                 read_ok = False
                 tp.controller_read_failed(f"{type(ex).__name__}: {ex}")
+        if finish_buttons and read_ok:
+            for ev in buttons:
+                if ev.type == pygame.JOYBUTTONDOWN and ev.button in finish_buttons:
+                    if tp.busy or tp.hold is not None or tp.halt_pending is not None:
+                        tp.out("[補正] 動いている間は終われません。止まってから □ を押してください")
+                    else:
+                        return "finish_button"
+            buttons = [ev for ev in buttons if ev.button not in finish_buttons]
         if read_ok:
             for ev in buttons:
                 if ev.type == pygame.JOYBUTTONDOWN:
@@ -1473,6 +1484,42 @@ def draw(screen, fonts, tp, mode_text, js):
 # main
 # =============================================================================
 
+def make_fonts():
+    mono = "menlo,consolas,dejavusansmono"
+    return {"big": pygame.font.SysFont(mono, 34, bold=True), "mono": pygame.font.SysFont(mono, 20),
+            "small": pygame.font.SysFont(mono, 16)}
+
+
+def run_embedded(arm, start, log, grip=None, mode_text="PAD CORRECTION", finish_buttons=(BTN_SQUARE,), out=print,
+                 on_frame=None):
+    """run_policy_client.py の推論のあとの確認で p を押したときに呼ぶ (2026-11-05 LOG450).
+    このファイルの main と同じ Teleop と run_loop を, 推論のプロセスの中で動かす (daemon のつながりと鍵は
+    推論と共有する. daemon は 1 本ずつしか受けないので, 別のプロセスでは動かせない).
+    □ (finish_buttons) で推論に戻る. ウィンドウを閉じても戻る. 操作・送り方・守りは main と同じ.
+    arm は DaemonArm (かその子) か DryArm. log は write(dict) / close() を持つもの.
+    grip: 推論の側の爪の状態 (1 = 閉 / 0 = 開). 画面の GRIP に出すだけ.
+    戻り値: (終わった理由, Teleop, 送る仕事が終わっているか)."""
+    tp = Teleop(arm, start, log, out=out)
+    if grip is not None:
+        tp.grip = "closed" if int(grip) == 1 else "open"
+    end_reason = None
+    worker_done = True
+    pygame.init()
+    pygame.joystick.init()
+    try:
+        screen = pygame.display.set_mode(WINDOW_SIZE)
+        pygame.display.set_caption(mode_text)
+        out("コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります")
+        end_reason = run_loop(tp, screen, make_fonts(), mode_text, finish_buttons=finish_buttons,
+                              on_frame=on_frame)
+    except KeyboardInterrupt:
+        end_reason = "ctrl_c"
+    finally:
+        worker_done = tp.shutdown()
+        pygame.quit()
+    return end_reason, tp, worker_done
+
+
 def git_info(path):
     def run(*a):
         try:
diff --git a/tools/test_gamepad_teleop.py b/tools/test_gamepad_teleop.py
index cc07f07..4c44327 100644
--- a/tools/test_gamepad_teleop.py
+++ b/tools/test_gamepad_teleop.py
@@ -1403,6 +1403,57 @@ class TestDryRunMain(unittest.TestCase):
         self.assertTrue(all(r["execute"] is False for r in rows))
 
 
+class TestEmbeddedFinish(unittest.TestCase):
+    """run_policy_client の p から呼ぶ run_embedded (2026-11-05 LOG450): □ で戻る. 動いている間の □ は断る."""
+
+    def test_square_finishes_only_when_idle(self):
+        import pygame
+
+        def btn(b, down=True):
+            t = pygame.JOYBUTTONDOWN if down else pygame.JOYBUTTONUP
+            pygame.event.post(pygame.event.Event(t, instance_id=7, button=b, joy=0))
+
+        seen = {}
+
+        def on_frame(n, tp):
+            if n == 1:
+                pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0))
+            elif n == 3:
+                btn(gt.BTN_RIGHT); btn(gt.BTN_RIGHT, False)
+            elif n == 5:
+                seen["busy_at_square"] = tp.busy
+                btn(gt.BTN_SQUARE); btn(gt.BTN_SQUARE, False)
+            elif n == 80:
+                btn(gt.BTN_SQUARE); btn(gt.BTN_SQUARE, False)
+            elif n == 400:
+                pygame.event.post(pygame.event.Event(pygame.QUIT))
+
+        rows, out = [], []
+
+        class L:
+            def write(self, r):
+                json.dumps(r, ensure_ascii=False)
+                rows.append(r)
+
+            def close(self):
+                pass
+
+        arm = gt.DryArm(HOME)
+        with mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
+                mock.patch.object(gt, "GRIPPER_SLEEP", 0.0):
+            reason, tp, done = gt.run_embedded(arm, np.array(HOME, dtype=float), L(), grip=1,
+                                               out=out.append, on_frame=on_frame)
+        self.assertEqual(reason, "finish_button")
+        self.assertTrue(done)
+        self.assertTrue(seen["busy_at_square"])
+        self.assertTrue(any("動いている間は終われません" in x for x in out))
+        self.assertEqual(tp.grip, "closed")
+        sent = [r for r in rows if r["kind"] == "move" and r.get("sent") is True]
+        self.assertEqual(len(sent), 1)
+        self.assertNotIn("square", [r.get("button_name") for r in rows])     # □ は Teleop に渡さない
+        self.assertAlmostEqual(arm.q[0], HOME[0] + gt.STEP_FINE_DEG)
+
+
 class TestExecuteMainRecords(unittest.TestCase):
     """--execute は今までどおり KIOXIA (ここでは一時フォルダ) に log.jsonl・meta.json・写しを書く."""
```


## 11/6 18:35 corr2 で Pod を立てた（lab_start.sh）

実機の前に、上カメラ（スマホ）・Pi の電源・ゲームパッドの Bluetooth をつないでから、Mac で次の1行を打ちました。

```
cd ~/r2r2r-scripts && ./lab_start.sh --gpu A40 --log Log22 --model corr2
```

Pod を作ってからサーバーが待ち受けるまでは214秒でした（Pod を作る2秒・立ち上がり28秒・スクリプトを送る56秒・鍵10秒・モデルを取る32秒・openpi 14秒・uv sync 21秒・設定2秒・サーバーの起動44秒・トンネル2秒・デーモンの確かめ3秒）。最初の推論だけ 52,970ms かかりました。サーバーが最初の1回だけ、計算の組み立て（コンパイル）をするためです。2回目からは中央の値で691ms でした。

#### Pod の準備の端末の記録 lab_start_20261106_183505.txt（1〜454行目）

```text
[LAB   ] 2026-11-06 18:35:05  GPU=NVIDIA A40  episode=run_0001_20261019_194818  log=/Volumes/KIOXIA/r2r2r-data/lab/Log22  
[MODEL ] --model corr2: corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (依頼 57 / 58)
         PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → /workspace/ckpt/corr_v2_1499  設定 pi05_mecharm_trim_corr_v4_lora  assets local/mecharm_redblock_box_teach_v1_trim2_corr_v4b

=== 1. Pod を作る
[POD   ] 作った: id b6j7nc4m74h7ru  GPU=NVIDIA A40  1時間 0.59 ドル  (記録 /Volumes/KIOXIA/r2r2r-data/lab/Log22/pod/lab_pod.json)
    (2 秒)

=== 2. Pod が立ち上がるまで待つ
[POD   ] SSH の IP とポートを API から受け取った (<POD_IP>:<SSH_PORT>)
[POD   ] 入れた: cbf7cd00209d
NVIDIA A40, 46068 MiB
    (28 秒)

=== 3a. スクリプトを送る
    (56 秒)

=== 3b. huggingface_hub を入れ, 鍵を渡す
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
[90mHint: The `hf-cli` skill is not installed. Run `hf skills add -g` to teach your AI agents how to use the `hf` CLI.[0m
[32m✓ Logged in[0m
  user: PixelstoMotors
    (10 秒)

=== 3c. モデルを取る (hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)

Fetching 28 files:   0%|          | 0/28 [00:00<?, ?it/s]
Fetching 28 files:   4%|▎         | 1/28 [00:00<00:02, 10.00it/s]
Fetching 28 files:  32%|███▏      | 9/28 [00:00<00:00, 38.45it/s]
Fetching 28 files:  46%|████▋     | 13/28 [00:10<00:15,  1.05s/it]
Fetching 28 files:  50%|█████     | 14/28 [00:10<00:13,  1.08it/s]
Fetching 28 files:  61%|██████    | 17/28 [00:13<00:10,  1.07it/s]
Fetching 28 files:  68%|██████▊   | 19/28 [00:15<00:08,  1.01it/s]
Fetching 28 files:  71%|███████▏  | 20/28 [00:15<00:06,  1.17it/s]
Fetching 28 files:  79%|███████▊  | 22/28 [00:16<00:03,  1.57it/s]
Fetching 28 files:  82%|████████▏ | 23/28 [00:17<00:03,  1.27it/s]
Fetching 28 files:  86%|████████▌ | 24/28 [00:18<00:03,  1.33it/s]
Fetching 28 files:  89%|████████▉ | 25/28 [00:18<00:01,  1.59it/s]
Fetching 28 files:  93%|█████████▎| 26/28 [00:19<00:01,  1.22it/s]
Fetching 28 files:  96%|█████████▋| 27/28 [00:20<00:00,  1.39it/s]
Fetching 28 files: 100%|██████████| 28/28 [00:28<00:00,  2.60s/it][32m✓ Downloaded[0m
  path: /workspace/ckpt/corr_v2_1499

Fetching 28 files: 100%|██████████| 28/28 [00:28<00:00,  1.00s/it]
6.0G	/workspace/ckpt/corr_v2_1499
    (32 秒)

=== 3d. openpi を取る (215abfb)
215abfb docs(droid): fix config search instruction (#1023)
    (14 秒)

=== 3e. uv sync
downloading uv 0.13.7 x86_64-unknown-linux-gnu
WARN: The following commands are shadowed by other commands in your PATH: uv uvx
Using CPython 3.11.13 interpreter at: /usr/bin/python3.11
Creating virtual environment at: .venv
Resolved 279 packages in 5ms
   Building openpi-client @ file:///workspace/openpi/packages/openpi-client
   Building openpi @ file:///workspace/openpi
   Updating https://github.com/huggingface/lerobot (0cf864870cf29f4738d3ade893e6fd13fbd7cdb5)
Downloading virtualenv (5.8MiB)
Downloading sympy (6.0MiB)
Downloading imageio-ffmpeg (28.1MiB)
Downloading labmaze (4.7MiB)
Downloading lxml (4.7MiB)
Downloading pillow (4.4MiB)
Downloading nvidia-cufft-cu12 (190.9MiB)
Downloading opencv-python-headless (47.7MiB)
Downloading numpy (17.4MiB)
Downloading llvmlite (40.4MiB)
Downloading nvidia-cublas-cu12 (374.9MiB)
Downloading matplotlib (8.2MiB)
Downloading scipy (35.9MiB)
Downloading ruff (11.0MiB)
Downloading av (34.7MiB)
Downloading nvidia-cuda-nvrtc-cu12 (22.6MiB)
Downloading hf-xet (4.9MiB)
Downloading nvidia-cuda-cupti-cu12 (8.5MiB)
Downloading nvidia-cudnn-cu12 (544.5MiB)
Downloading nvidia-cusparse-cu12 (206.5MiB)
Downloading nvidia-curand-cu12 (53.7MiB)
Downloading pyarrow (40.3MiB)
Downloading pandas (12.5MiB)
Downloading nvidia-nccl-cu12 (192.0MiB)
Downloading dm-control (37.5MiB)
Downloading opencv-python (60.1MiB)
Downloading nvidia-cusolver-cu12 (150.9MiB)
Downloading polars (34.6MiB)
Downloading torchvision (7.1MiB)
Downloading transformers (10.3MiB)
Downloading jax-cuda12-pjrt (99.8MiB)
Downloading rerun-sdk (68.1MiB)
Downloading numcodecs (8.4MiB)
Downloading tensorstore (17.5MiB)
Downloading cmake (26.6MiB)
Downloading jax-cuda12-plugin (15.9MiB)
Downloading wandb (20.4MiB)
Downloading nvidia-nvjitlink-cu12 (18.8MiB)
Downloading fonttools (4.7MiB)
Downloading mujoco (4.4MiB)
Downloading triton (148.5MiB)
Downloading jaxlib (100.2MiB)
Downloading nvidia-cusparselt-cu12 (149.5MiB)
Downloading torch (783.1MiB)
Downloading h5py (4.3MiB)
Downloading numba (3.7MiB)
Downloading pyopengl (3.0MiB)
Downloading hf-transfer (3.4MiB)
Downloading diffusers (3.4MiB)
Downloading nvidia-cuda-nvcc-cu12 (38.7MiB)
 Downloaded pyopengl
Downloading debugpy (3.0MiB)
 Downloaded hf-transfer
 Downloaded diffusers
Downloading tokenizers (2.9MiB)
Downloading jax (2.3MiB)
 Downloaded numba
Downloading widgetsnbextension (2.1MiB)
 Downloaded mujoco
 Downloaded pillow
Downloading networkx (1.9MiB)
Downloading ml-dtypes (2.1MiB)
 Downloaded h5py
 Downloaded labmaze
 Downloaded fonttools
Downloading pydantic-core (1.9MiB)
Downloading jedi (1.5MiB)
 Downloaded hf-xet
Downloading aiohttp (1.6MiB)
Downloading kiwisolver (1.4MiB)
 Downloaded virtualenv
Downloading torchcodec (1.3MiB)
 Downloaded lxml
 Downloaded sympy
Downloading sentencepiece (1.2MiB)
 Downloaded widgetsnbextension
Downloading pygments (1.2MiB)
 Downloaded jax
Downloading setuptools (1.1MiB)
Downloading nvidia-cufile-cu12 (1.1MiB)
 Downloaded kiwisolver
 Downloaded networkx
 Downloaded ml-dtypes
 Downloaded pydantic-core
 Downloaded aiohttp
 Downloaded jedi
Downloading pymunk (1.0MiB)
 Downloaded debugpy
 Downloaded tokenizers
 Downloaded torchvision
 Downloaded torchcodec
 Downloaded sentencepiece
 Downloaded pygments
 Downloaded setuptools
 Downloaded nvidia-cufile-cu12
 Downloaded pymunk
 Downloaded matplotlib
 Downloaded numcodecs
 Downloaded nvidia-cuda-cupti-cu12
   Building evdev==1.9.2
 Downloaded transformers
 Downloaded ruff
   Building antlr4-python3-runtime==4.9.3
 Downloaded pandas
 Downloaded jax-cuda12-plugin
 Downloaded numpy
 Downloaded tensorstore
 Downloaded nvidia-nvjitlink-cu12
      Built openpi-client @ file:///workspace/openpi/packages/openpi-client
      Built openpi @ file:///workspace/openpi
 Downloaded wandb
 Downloaded nvidia-cuda-nvrtc-cu12
 Downloaded imageio-ffmpeg
      Built antlr4-python3-runtime==4.9.3
 Downloaded cmake
 Downloaded polars
 Downloaded av
 Downloaded scipy
 Downloaded llvmlite
 Downloaded pyarrow
 Downloaded nvidia-cuda-nvcc-cu12
 Downloaded dm-control
 Downloaded opencv-python-headless
 Downloaded nvidia-curand-cu12
    Updated https://github.com/huggingface/lerobot (0cf864870cf29f4738d3ade893e6fd13fbd7cdb5)
   Building lerobot @ git+https://github.com/huggingface/lerobot@0cf864870cf29f4738d3ade893e6fd13fbd7cdb5
 Downloaded opencv-python
      Built evdev==1.9.2
 Downloaded rerun-sdk
      Built lerobot @ git+https://github.com/huggingface/lerobot@0cf864870cf29f4738d3ade893e6fd13fbd7cdb5
 Downloaded jaxlib
 Downloaded jax-cuda12-pjrt
 Downloaded nvidia-cusparselt-cu12
 Downloaded nvidia-cusolver-cu12
 Downloaded triton
 Downloaded nvidia-nccl-cu12
 Downloaded nvidia-cufft-cu12
 Downloaded nvidia-cusparse-cu12
 Downloaded nvidia-cublas-cu12
 Downloaded nvidia-cudnn-cu12
 Downloaded torch
Prepared 240 packages in 14.77s
Installed 240 packages in 1.79s
 + absl-py==2.3.0
 + aiohappyeyeballs==2.6.1
 + aiohttp==3.12.4
 + aiosignal==1.3.2
 + annotated-types==0.7.0
 + antlr4-python3-runtime==4.9.3
 + asttokens==3.0.0
 + attrs==25.3.0
 + augmax==0.4.1
 + av==17.0.0
 + beartype==0.19.0
 + beautifulsoup4==4.13.4
 + blinker==1.9.0
 + cachetools==5.5.2
 + certifi==2025.4.26
 + cffi==1.17.1
 + cfgv==3.4.0
 + charset-normalizer==3.4.2
 + chex==0.1.90
 + click==8.2.1
 + cloudpickle==3.1.1
 + cmake==4.0.2
 + comm==0.2.2
 + contourpy==1.3.2
 + crc32c==2.7.1
 + cycler==0.12.1
 + datasets==3.6.0
 + debugpy==1.8.14
 + decorator==5.2.1
 + deepdiff==8.5.0
 + diffusers==0.33.1
 + dill==0.3.8
 + distlib==0.3.9
 + dm-control==1.0.14
 + dm-env==1.6
 + dm-tree==0.1.9
 + docker-pycreds==0.4.0
 + docstring-parser==0.16
 + donfig==0.8.1.post1
 + draccus==0.10.0
 + einops==0.8.1
 + equinox==0.12.2
 + etils==1.12.2
 + evdev==1.9.2
 + executing==2.2.0
 + farama-notifications==0.0.4
 + filelock==3.18.0
 + flask==3.1.1
 + flatbuffers==25.2.10
 + flax==0.10.2
 + fonttools==4.58.1
 + frozenlist==1.6.0
 + fsspec==2025.3.0
 + gcsfs==2025.3.0
 + gdown==5.2.0
 + gitdb==4.0.12
 + gitpython==3.1.44
 + glfw==2.9.0
 + google-api-core==2.24.2
 + google-auth==2.40.2
 + google-auth-oauthlib==1.2.2
 + google-cloud-core==2.4.3
 + google-cloud-storage==3.1.0
 + google-crc32c==1.7.1
 + google-resumable-media==2.7.2
 + googleapis-common-protos==1.70.0
 + gym-aloha==0.1.1
 + gymnasium==0.29.1
 + h5py==3.13.0
 + hf-transfer==0.1.9
 + hf-xet==1.1.2
 + huggingface-hub==0.32.3
 + humanize==4.12.3
 + identify==2.6.12
 + idna==3.10
 + imageio==2.37.0
 + imageio-ffmpeg==0.6.0
 + importlib-metadata==8.7.0
 + importlib-resources==6.5.2
 + iniconfig==2.1.0
 + inquirerpy==0.3.4
 + ipykernel==6.29.5
 + ipython==9.2.0
 + ipython-pygments-lexers==1.1.1
 + ipywidgets==8.1.7
 + itsdangerous==2.2.0
 + jax==0.5.3
 + jax-cuda12-pjrt==0.5.3
 + jax-cuda12-plugin==0.5.3
 + jaxlib==0.5.3
 + jaxtyping==0.2.36
 + jedi==0.19.2
 + jinja2==3.1.6
 + jsonlines==4.0.0
 + jupyter-client==8.6.3
 + jupyter-core==5.8.1
 + jupyterlab-widgets==3.0.15
 + kiwisolver==1.4.8
 + labmaze==1.0.6
 + lerobot==0.1.0 (from git+https://github.com/huggingface/lerobot@0cf864870cf29f4738d3ade893e6fd13fbd7cdb5)
 + llvmlite==0.44.0
 + lxml==5.4.0
 + markdown-it-py==3.0.0
 + markupsafe==3.0.2
 + matplotlib==3.10.3
 + matplotlib-inline==0.1.7
 + mdurl==0.1.2
 + mergedeep==1.3.4
 + ml-collections==1.0.0
 + ml-dtypes==0.4.1
 + mpmath==1.3.0
 + msgpack==1.1.0
 + mujoco==2.3.7
 + multidict==6.4.4
 + multiprocess==0.70.16
 + mypy-extensions==1.1.0
 + nest-asyncio==1.6.0
 + networkx==3.5
 + nodeenv==1.9.1
 + numba==0.61.2
 + numcodecs==0.16.1
 + numpy==1.26.4
 + numpydantic==1.6.9
 + nvidia-cublas-cu12==12.6.4.1
 + nvidia-cuda-cupti-cu12==12.6.80
 + nvidia-cuda-nvcc-cu12==12.9.41
 + nvidia-cuda-nvrtc-cu12==12.6.77
 + nvidia-cuda-runtime-cu12==12.6.77
 + nvidia-cudnn-cu12==9.5.1.17
 + nvidia-cufft-cu12==11.3.0.4
 + nvidia-cufile-cu12==1.11.1.6
 + nvidia-curand-cu12==10.3.7.77
 + nvidia-cusolver-cu12==11.7.1.2
 + nvidia-cusparse-cu12==12.5.4.2
 + nvidia-cusparselt-cu12==0.6.3
 + nvidia-ml-py==12.575.51
 + nvidia-nccl-cu12==2.26.2
 + nvidia-nvjitlink-cu12==12.6.85
 + nvidia-nvtx-cu12==12.6.77
 + oauthlib==3.2.2
 + omegaconf==2.3.0
 + opencv-python==4.11.0.86
 + opencv-python-headless==4.11.0.86
 + openpi==0.1.0 (from file:///workspace/openpi)
 + openpi-client==0.1.0 (from file:///workspace/openpi/packages/openpi-client)
 + opt-einsum==3.4.0
 + optax==0.2.4
 + orbax-checkpoint==0.11.13
 + orderly-set==5.4.1
 + packaging==25.0
 + pandas==2.2.3
 + parso==0.8.4
 + pexpect==4.9.0
 + pfzy==0.3.4
 + pillow==11.2.1
 + platformdirs==4.3.8
 + pluggy==1.6.0
 + polars==1.30.0
 + pre-commit==4.2.0
 + prompt-toolkit==3.0.51
 + propcache==0.3.1
 + proto-plus==1.26.1
 + protobuf==4.25.8
 + psutil==7.0.0
 + ptyprocess==0.7.0
 + pure-eval==0.2.3
 + pyarrow==20.0.0
 + pyasn1==0.6.1
 + pyasn1-modules==0.4.2
 + pycparser==2.22
 + pydantic==2.11.5
 + pydantic-core==2.33.2
 + pygments==2.19.1
 + pymunk==7.0.0
 + pynput==1.8.1
 + pynvml==12.0.0
 + pyopengl==3.1.9
 + pyparsing==3.2.3
 + pysocks==1.7.1
 + pytest==8.3.5
 + python-dateutil==2.9.0.post0
 + python-xlib==0.33
 + pytz==2025.2
 + pyyaml==6.0.2
 + pyyaml-include==1.4.1
 + pyzmq==26.4.0
 + regex==2024.11.6
 + requests==2.32.3
 + requests-oauthlib==2.0.0
 + rerun-sdk==0.23.1
 + rich==14.0.0
 + rsa==4.9.1
 + ruff==0.11.12
 + safetensors==0.5.3
 + scipy==1.15.3
 + sentencepiece==0.2.0
 + sentry-sdk==2.29.1
 + setproctitle==1.3.6
 + setuptools==80.9.0
 + shtab==1.7.2
 + simplejson==3.20.1
 + six==1.17.0
 + smmap==5.0.2
 + soupsieve==2.7
 + stack-data==0.6.3
 + sympy==1.14.0
 + tensorstore==0.1.74
 + termcolor==3.1.0
 + tokenizers==0.21.1
 + toml==0.10.2
 + toolz==1.0.0
 + torch==2.7.1
 + torchcodec==0.4.0
 + torchvision==0.22.1
 + tornado==6.5.1
 + tqdm==4.67.1
 + tqdm-loggable==0.2
 + traitlets==5.14.3
 + transformers==4.53.2
 + treescope==0.1.9
 + triton==3.3.1
 + typeguard==4.4.2
 + typing-extensions==4.13.2
 + typing-inspect==0.9.0
 + typing-inspection==0.4.1
 + tyro==0.9.22
 + tzdata==2025.2
 + urllib3==2.4.0
 + virtualenv==20.31.2
 + wadler-lindig==0.1.6
 + wandb==0.19.11
 + wcwidth==0.2.13
 + websockets==15.0.1
 + werkzeug==3.1.3
 + widgetsnbextension==4.0.14
 + wrapt==1.14.1
 + xxhash==3.5.0
 + yarl==1.20.0
 + zarr==3.0.8
 + zipp==3.22.0
    (21 秒)

=== 3f. addon・設定・assets (pi05_mecharm_trim_corr_v4_lora)
installed: policies/mecharm_policy.py + config.py (pi05_mecharm_lora)
installed: pi05_mecharm_trim_lora (repo_id=local/mecharm_redblock_box_teach_v1_trim2, 4,000 steps)
installed: pi05_mecharm_trim_corr_v4_lora (repo_id=local/mecharm_redblock_box_teach_v1_trim2_corr_v4b, 1500 steps, init=/workspace/ckpt/corr_v1_1499/params)
    (2 秒)

=== 4. サーバーの起動
[SERVE ] --model corr2: --policy.config=pi05_mecharm_trim_corr_v4_lora --policy.dir=/workspace/ckpt/corr_v2_1499
INFO:websockets.server:server listening on 0.0.0.0:8000
```


## 実機の1〜9回目（18:39〜18:58）：成功5回・失敗4回、直しは19回ともゲームパッド

9回の結果と直しの中身は次のとおりです。直しの欄の角度は、直しを始めたときと終えたときの実測の差（1°未満の関節は省いた）です。

| 回 | 記録 | 結果 | 1つ目の直し（つかむ前） | 2つ目の直し（つかんだあと） |
|---|---|---|---|---|
| 1 | pi05_20261106_183926 | 成功 | 推論3のあと 17.9秒・j0 +2.1° | 推論8のあと 33.4秒・j1 −32.5°、j4 +17.2° |
| 2 | pi05_20261106_184312 | 失敗 | 推論3のあと 13.2秒・j0 +2.0° | 推論8のあと 12.7秒・j1 −26.7° |
| 3 | pi05_20261106_184452 | 成功 | 推論3のあと 20.1秒・j0 +3.3° | 推論6のあと 11.1秒・j1 −25.2° |
| 4 | pi05_20261106_184650 | 成功 | 推論3のあと 9.8秒・j0 +1.2° | 推論7のあと 26.6秒・j1 −43.8°、j2 +17.9° |
| 5 | pi05_20261106_184906 | 成功 | 推論3のあと 9.4秒・j0 +2.1° | 推論7のあと 13.4秒・j1 −24.8° |
| 6 | pi05_20261106_185050 | 失敗 | 推論3のあと 13.3秒・j0 +3.2° | 推論7のあと 15.9秒・j1 −29.2° |
| 7 | pi05_20261106_185225 | 失敗 | 推論3のあと 8.9秒・j0 +2.0° | 推論7と8のあとの2回 12.1秒・10.4秒・j1 −12.8° |
| 8 | pi05_20261106_185415 | 失敗 | 推論3のあと 9.8秒・j0 +2.1° | 推論8のあと 18.0秒・j1 −12.0° |
| 9 | pi05_20261106_185554 | 成功 | 推論3のあと 9.9秒・j0 +3.0° | 推論6のあと 15.0秒・j1 −24.1°、j4 +6.2° |

どの回も流れは同じでした。AI がブロックの上まで下りてきたところ（推論3のあと）で、爪の真ん中にブロックが来るよう、十字キーの左右で根元 j0 を1°ずつ2〜3回回しました。AI が爪を閉じたあと（推論4か5）、持ち上げが足りないまま箱の方へ向かう動きになったところで、十字キーの上を長押しして肩 j1 を戻し（j1 は小さいほどアームが上がる）、ブロックを持ち上げました。そのあとは、成功した5回とも AI が箱の上まで運び、爪を開いて、HOME の方へ戻るところまで自分で続けています。

失敗の4回は、つかんだあとにブロックが落ちて、そこで h（HOME に戻して終える）を押しました。落ちたときの様子は、このあとの節にまとめています。6回目の結果の入力は、記録の画面に `y n` と残っていますが、y を消して n を打ち直したもので、記録（`pi05_20261106_185050.json`）は失敗です。

ゲームパッドの画面を開くたびに、ターミナルに `objc[...]: Class ... is implemented in both ...cv2/.dylibs/libSDL2... and ...pygame/.dylibs/libSDL2...` という警告が十数行出ます。カメラの読み込み（cv2）と pygame が、同じ部品を別々に持っているためです。LOG436 から出ているもので、動きには影響していません。

### 1回目 18:39 の端末の記録（成功・lab_start_20261106_183505.txt 455〜805行目）

#### 1回目（pi05_20261106_183926）

```text
    (44 秒)

=== 5. Mac の 8000 番へのトンネル
[TUNNEL] localhost:8000 → Pod の 8000 番 (pid 53112. このスクリプトが終わると閉じる)
    (2 秒)

=== 6. デーモンの状態
--- process
509 /usr/bin/python3 -u /home/ubuntu/mecharm_daemon.py
--- 5555
LISTEN   0         1                   0.0.0.0:5555             0.0.0.0:*       
--- systemd
enabled: enabled
active:  active
    (3 秒)

=== 7. HOME → 爪を開く → 位置合わせ (1 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.61, 92.90, -9.05, 4.92, -85.78, -29.79]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=79.89°
[WAIT] t=1.1s  max_err=64.16°
[WAIT] t=1.6s  max_err=48.69°
[WAIT] t=2.1s  max_err=33.13°
[WAIT] t=2.7s  max_err=17.31°
[WAIT] t=3.2s  max_err=1.66°
[DONE] HOME 到達 max_err=1.66°
[POST] 最終姿勢: [-0.61, 1.49, 0.79, 0.52, 1.23, -29.88]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +5.5  dx=  -8.4  response=0.724  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +6.8  dx=  -8.1  response=0.744  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +9.4  dx=  -7.7  response=0.679  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy= +13.6  dx=  -9.2  response=0.714  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy= +18.5  dx=  -9.2  response=0.636  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy= +22.2  dx= -10.3  response=0.490  → ずれ (|dy|=22.2 > 20)
[ALIGN ] dy= +17.1  dx=  -9.7  response=0.517  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +7.0  dx=  -9.0  response=0.742  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +3.0  dx=  -8.5  response=0.819  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +1.5  dx=  -5.4  response=0.769  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +0.1  dx=  -3.5  response=0.859  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -1.7  dx=  -2.8  response=0.835  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -3.2  dx=  +0.0  response=0.833  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -4.8  dx=  +3.4  response=0.821  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -6.2  dx=  +3.1  response=0.776  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -3.1  dx=  -1.3  response=0.797  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -1.5  dx=  -4.8  response=0.873  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -4.0  dx= -12.2  response=0.785  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -6.3  dx=  -8.0  response=0.879  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -4.9  dx=  -0.8  response=0.844  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -2.8  dx=  +6.5  response=0.874  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -4.0  dx=  +6.0  response=0.890  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -2.8  dx=  +5.1  response=0.860  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +0.1  dx=  +5.5  response=0.852  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +3.7  dx=  +3.4  response=0.734  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +8.5  dx=  +5.0  response=0.699  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +9.6  dx=  +3.7  response=0.695  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +7.4  dx=  +4.2  response=0.687  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +4.7  dx=  +4.2  response=0.777  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +2.8  dx=  +3.3  response=0.801  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +0.7  dx=  +0.7  response=0.823  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -1.6  dx=  -5.5  response=0.801  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -3.1  dx= -16.4  response=0.718  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -4.1  dx= -12.6  response=0.819  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -5.9  dx=  +2.0  response=0.875  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -4.2  dx=  -3.8  response=0.835  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  -0.6  dx=  -8.2  response=0.784  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +2.3  dx=  -9.5  response=0.814  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +3.6  dx= -10.0  response=0.801  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[ALIGN ] dy=  +2.0  dx= -10.5  response=0.795  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (47 秒)

=== 8. 実機推論 (1 回目)
[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_183926
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.6 dx=+6.0 response=0.575 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_183926/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 52970ms  10ステップ先  Δ= +3.3 +16.2 +11.0  -2.4 -23.4  +2.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.13,   +5.67,   +3.34,   -0.11,   -5.88,  -29.70]  speed=20 wait=0.0s  (pre max|Δ|=5.62°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.58° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.7  +4.4  +2.6  -0.4  -5.6  +0.2  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.09,  +10.48,   +6.74,   -0.69,  -12.99,  -29.05]  speed=20 wait=0.0s  (pre max|Δ|=8.69°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.66° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.1  +4.0  +2.5  -0.9  -8.7  +0.8  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +2.38,  +15.66,  +10.49,   -1.79,  -21.06,  -28.01]  speed=20 wait=0.0s  (pre max|Δ|=9.64°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.47° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.2  +4.3  +2.9  -1.4  -9.7  +1.8  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +2.74,  +17.44,  +11.77,   -2.15,  -23.69,  -27.66]  speed=20 wait=0.0s  (pre max|Δ|=4.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.16° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.5  +0.8  +0.4  -0.3  -4.1  +1.1  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.37°（関節 j4）
[001] 推論 547ms  10ステップ先  Δ= +5.4 +14.4  +9.9  -0.8 -23.9  +1.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +3.89,  +22.01,  +14.68,   -2.59,  -30.39,  -27.08]  speed=20 wait=0.0s  (pre max|Δ|=7.98°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.84° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.1  +3.7  +2.3  -0.6  -8.0  +1.1  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +5.34,  +26.34,  +17.58,   -2.64,  -37.23,  -26.90]  speed=20 wait=0.0s  (pre max|Δ|=7.62°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.20° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.6  +3.5  +2.1  +0.3  -7.6  +0.5  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.02,  +30.89,  +20.86,   -2.78,  -44.01,  -26.84]  speed=20 wait=0.0s  (pre max|Δ|=7.80°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.04° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.8  +3.7  +2.7  +0.6  -7.9  +0.5  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +7.46,  +32.69,  +21.86,   -2.78,  -46.24,  -26.80]  speed=20 wait=0.0s  (pre max|Δ|=3.18°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.79° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +0.9  +0.5  +0.6  -3.2  +0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.79°（関節 j1）
[002] 推論 629ms  10ステップ先  Δ= +3.3 +10.3  +6.9  +0.7 -15.6  +0.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +8.74,  +37.54,  +24.99,   -2.78,  -53.24,  -26.74]  speed=20 wait=0.0s  (pre max|Δ|=7.19°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.95° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.5  +4.1  +2.7  +0.6  -7.2  +0.3  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [  +9.96,  +41.26,  +27.41,   -2.81,  -58.51,  -26.70]  speed=20 wait=0.0s  (pre max|Δ|=6.13°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.83° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.3  +2.8  +2.0  +0.4  -6.1  +0.4  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.69,  +43.77,  +29.10,   -2.82,  -61.37,  -26.66]  speed=20 wait=0.0s  (pre max|Δ|=3.46°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.07° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.8  +1.7  +1.5  +0.2  -3.5  +0.3  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.74,  +43.74,  +29.08,   -2.83,  -61.60,  -26.64]  speed=20 wait=0.0s  (pre max|Δ|=0.90°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.90° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.4  -0.9  +0.2  -0.0  -0.6  +0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.90°（関節 j1）
[003] 推論 1532ms  10ステップ先  Δ= +2.4  +4.6  -1.0  -0.0  -2.2  +0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53153]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d2792c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d0248e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d2793b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d0249d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d2794d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d2795c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d2795e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d279778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53153]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11d024d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12d2797c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +11.72 +44.64 +29.26 -2.81 -61.08 -26.80  届いた（ずれ 1.67°）  届いた角度 +11.86 +44.64 +29.26 -2.81 -62.75 -26.80
[送信] j0 +1.0° → +12.72 +44.64 +29.26 -2.81 -61.08 -26.80  届いた（ずれ 1.67°）  届いた角度 +12.74 +44.64 +29.26 -2.72 -62.75 -26.80
[PAD   ] 補正 1 を終えました (17.9 秒, 送った目標 2 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 555ms  10ステップ先  Δ= +1.5 +15.2  -8.2  -0.6  -2.9  -0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +12.06,  +51.44,  +28.18,   -2.60,  -65.27,  -26.69]  speed=20 wait=0.0s  (pre max|Δ|=6.80°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.35° (tol 3.0°)
       [VIA   ] step  2  Δ= -0.8  +6.8  -1.1  +0.0  -3.4  +0.1  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.14,  +55.49,  +25.16,   -2.71,  -65.57,  -26.74]  speed=20 wait=0.0s  (pre max|Δ|=4.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.03° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.8  +3.1  -4.0  -0.2  -2.5  +0.1  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.97,  +58.97,  +21.79,   -3.15,  -65.03,  -26.97]  speed=20 wait=0.0s  (pre max|Δ|=4.31°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.79° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.8  +2.5  -4.3  -0.6  -1.5  -0.2  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +14.32,  +59.79,  +21.04,   -3.20,  -64.86,  -27.09]  speed=20 wait=0.0s  (pre max|Δ|=1.46°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.46° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  +0.1  -1.5  -0.7  -0.6  -0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.37°（関節 j2）
[005] 推論 618ms  5ステップ先  Δ= +0.7  +0.8  -2.3  -1.0  +0.1  -0.1  グリッパー→閉
       [VIA   ] 中継点 2 点: step 2, 4
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +14.47,  +60.72,  +20.26,   -3.54,  -64.41,  -27.05]  speed=20 wait=0.0s  (pre max|Δ|=1.71°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.71° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  +0.2  -1.7  -0.9  +0.7  -0.1  着いた
[SEND] pi05_chunk005_via04: send_angles (single) [ +14.42,  +60.80,  +20.15,   -3.51,  -64.32,  -27.08]  speed=20 wait=0.0s  (pre max|Δ|=1.03°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.98° (tol 3.0°)
       [VIA   ] step  4  Δ= +0.5  -0.4  -1.3  -0.7  +1.0  -0.0  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 2.12°（関節 j4）
[006] 推論 631ms  10ステップ先  Δ= +0.7  -0.3  -5.1  -0.7  +1.3  +0.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +14.35,  +60.45,  +19.36,   -3.49,  -64.51,  -26.88]  speed=20 wait=0.0s  (pre max|Δ|=2.02°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.02° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  -0.9  -1.5  -0.5  +2.0  +0.5  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +14.39,  +60.60,  +17.40,   -3.58,  -64.58,  -26.57]  speed=20 wait=0.0s  (pre max|Δ|=3.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.84° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.4  -0.7  -3.4  -0.6  +2.0  +0.8  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +14.51,  +60.87,  +16.32,   -3.64,  -64.93,  -26.50]  speed=20 wait=0.0s  (pre max|Δ|=1.60°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.60° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  -0.5  -2.0  -0.5  -0.7  +0.6  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +14.64,  +61.06,  +15.88,   -3.63,  -65.10,  -26.48]  speed=20 wait=0.0s  (pre max|Δ|=2.04°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.74° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.7  -0.3  -2.0  -0.4  -1.2  +0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.74°（関節 j4）
[007] 推論 1544ms  10ステップ先  Δ= +0.6  -0.5  -7.2  -0.5  -2.5  +0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk007_via02: send_angles (single) [ +14.82,  +60.85,  +13.71,   -3.60,  -65.31,  -26.55]  speed=20 wait=0.0s  (pre max|Δ|=2.98°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.98° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.9  -0.8  -3.0  -0.3  -1.9  +0.2  着いた
[SEND] pi05_chunk007_via05: send_angles (single) [ +14.70,  +61.06,  +11.60,   -3.65,  -65.48,  -26.59]  speed=20 wait=0.0s  (pre max|Δ|=4.13°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.85° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  -0.6  -4.6  -0.4  -1.8  +0.1  着いた
[SEND] pi05_chunk007_via08: send_angles (single) [ +14.53,  +61.09,   +9.99,   -3.79,  -65.59,  -26.60]  speed=20 wait=0.0s  (pre max|Δ|=2.57°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.57° (tol 3.0°)
       [VIA   ] step  8  Δ= -0.1  -0.7  -2.6  +0.3  -2.0  -0.7  着いた
[SEND] pi05_chunk007_via09: send_angles (single) [ +14.56,  +61.24,   +9.59,   -3.79,  -65.83,  -26.60]  speed=20 wait=0.0s  (pre max|Δ|=2.36°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.20° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.0  -0.8  -2.5  +0.3  -2.2  -0.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.20°（関節 j4）
[008] 推論 554ms  10ステップ先  Δ= +0.1  -0.5  -7.9  +0.5  -2.0  -0.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 8 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +62.22° → +61.25° まで（目標を 1 回送信。止めます）
[長押し] j1 を +54.22° → +53.43° まで（目標を 1 回送信。止めます）
[送信] j1 -1.0° → +14.58 +40.92 +10.89 -4.13 -66.44 -25.92  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 1.0°）  届いた角度 +14.58 +41.92 +10.89 -4.04 -66.26 -26.27
[送信] j1 -1.0° → +14.58 +39.92 +10.89 -4.13 -66.44 -25.92  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 2.0°）  届いた角度 +14.58 +41.92 +10.89 -4.04 -66.18 -26.27
[送信] j1 -1.0° → +14.58 +38.92 +10.89 -4.13 -66.44 -25.92  手前で止まった（次の押しは この目標の先から）（ずれ 1.94°）  届いた角度 +14.58 +40.86 +10.89 -4.04 -65.74 -26.27
[長押し] j1 を +40.86° → +40.69° まで（目標を 1 回送信。止めます）
[関節] 上下で動かす関節を j2 にしました
[関節] 上下で動かす関節を j3 にしました
[関節] 上下で動かす関節を j4 にしました
[長押し] j4 を -65.30° → -61.25° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (33.4 秒, 送った目標 7 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[009] 推論 627ms  10ステップ先  Δ=+10.4  +9.1  +3.4  +2.0 -10.9  -0.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk009_via02: send_angles (single) [ +18.99,  +30.06,  +12.05,   -3.31,  -49.81,  -26.55]  speed=20 wait=0.0s  (pre max|Δ|=4.41°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.05° (tol 3.0°)
       [VIA   ] step  2  Δ= +4.4  +0.4  +0.4  +0.7  -0.2  -0.3  着いた
[SEND] pi05_chunk009_via05: send_angles (single) [ +23.60,  +32.19,  +13.02,   -2.53,  -52.47,  -26.88]  speed=20 wait=0.0s  (pre max|Δ|=4.80°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.06° (tol 3.0°)
       [VIA   ] step  5  Δ= +4.7  +1.1  +0.3  +1.0  -2.5  -0.6  着いた
[SEND] pi05_chunk009_via08: send_angles (single) [ +25.00,  +37.22,  +14.93,   -2.10,  -58.70,  -26.89]  speed=20 wait=0.0s  (pre max|Δ|=7.29°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.01° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.5  +4.2  +1.2  +0.6  -7.3  -0.6  着いた
[SEND] pi05_chunk009_via09: send_angles (single) [ +24.97,  +38.81,  +15.10,   -1.99,  -60.13,  -26.90]  speed=20 wait=0.0s  (pre max|Δ|=2.04°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.04° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.1  +0.6  -0.5  +0.7  -2.0  -0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.72°（関節 j4）
[010] 推論 579ms  10ステップ先  Δ=+15.7  +7.4 -10.7  +3.8  +2.1  -3.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk010_via02: send_angles (single) [ +33.16,  +38.98,  +10.84,   -0.71,  -57.78,  -27.99]  speed=20 wait=0.0s  (pre max|Δ|=8.20°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.80° (tol 3.0°)
       [VIA   ] step  2  Δ= +8.2  -0.8  -4.8  +1.8  +1.9  -1.6  着いた
[SEND] pi05_chunk010_via05: send_angles (single) [ +38.73,  +40.28,   +6.79,   +0.34,  -56.34,  -29.25]  speed=20 wait=0.0s  (pre max|Δ|=5.51°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.66° (tol 3.0°)
       [VIA   ] step  5  Δ= +5.3  +0.5  -5.5  +1.3  +3.2  -1.8  着いた
[SEND] pi05_chunk010_via08: send_angles (single) [ +40.60,  +45.08,   +5.24,   +0.94,  -56.97,  -29.66]  speed=20 wait=0.0s  (pre max|Δ|=3.86°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.67° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.5  +3.9  -2.8  +1.0  +1.0  -0.9  着いた
[SEND] pi05_chunk010_via09: send_angles (single) [ +40.69,  +46.68,   +4.97,   +1.13,  -57.32,  -29.71]  speed=20 wait=0.0s  (pre max|Δ|=2.14°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.88° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +1.3  -2.2  +0.2  +0.7  -0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.53°（関節 j2）
[011] 推論 549ms  10ステップ先  Δ= +0.1 +14.9  -3.2  +1.6 -10.4  -1.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk011_via02: send_angles (single) [ +40.95,  +51.34,   +3.98,   +1.41,  -58.78,  -29.96]  speed=20 wait=0.0s  (pre max|Δ|=3.36°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.55° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  +3.4  -2.3  +0.5  -0.8  -0.7  着いた
[SEND] pi05_chunk011_via05: send_angles (single) [ +40.94,  +57.37,   +3.63,   +1.70,  -63.91,  -30.22]  speed=20 wait=0.0s  (pre max|Δ|=5.82°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.73° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  +4.9  -1.9  +0.6  -5.8  -0.8  着いた
[SEND] pi05_chunk011_via08: send_angles (single) [ +41.08,  +61.45,   +3.30,   +1.89,  -67.97,  -30.24]  speed=20 wait=0.0s  (pre max|Δ|=5.13°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.27° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.7  +3.0  -2.1  +0.6  -5.1  -0.4  着いた
[SEND] pi05_chunk011_via09: send_angles (single) [ +41.09,  +61.79,   +3.30,   +1.95,  -68.44,  -30.25]  speed=20 wait=0.0s  (pre max|Δ|=1.38°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.38° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.1  -0.8  -1.3  +0.4  -1.4  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.38°（関節 j4）
[012] 推論 1483ms  5ステップ先  Δ= -0.1  +1.9  -1.4  +0.5  -4.8  -0.4  グリッパー→開
       [VIA   ] 中継点 2 点: step 2, 4
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk012_via02: send_angles (single) [ +40.93,  +64.75,   +3.16,   +2.11,  -71.49,  -30.25]  speed=20 wait=0.0s  (pre max|Δ|=4.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.41° (tol 3.0°)
       [VIA   ] step  2  Δ= -0.0  +2.0  -1.4  +0.5  -4.4  -0.4  着いた
[SEND] pi05_chunk012_via04: send_angles (single) [ +40.86,  +64.67,   +3.14,   +2.16,  -71.83,  -30.23]  speed=20 wait=0.0s  (pre max|Δ|=1.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.43° (tol 3.0°)
       [VIA   ] step  4  Δ= -0.1  -1.1  -1.4  +0.3  -0.5  +0.1  着いた
  [Pi] GRIPPER: OPEN result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.43°（関節 j2）
[013] 推論 598ms  10ステップ先  Δ= -0.0 -17.2  -1.4  +0.4 +16.3  -0.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk013_via02: send_angles (single) [ +40.87,  +60.06,   +3.15,   +2.22,  -67.71,  -30.40]  speed=20 wait=0.0s  (pre max|Δ|=5.68°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.95° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.0  -5.7  -1.4  +0.3  +3.6  +0.1  着いた
[SEND] pi05_chunk013_via05: send_angles (single) [ +41.14,  +55.14,   +2.99,   +2.19,  -62.26,  -30.56]  speed=20 wait=0.0s  (pre max|Δ|=8.40°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.77° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.3  -7.0  -1.5  +0.2  +8.4  -0.1  着いた
[SEND] pi05_chunk013_via08: send_angles (single) [ +41.12,  +49.93,   +3.08,   +2.31,  -56.31,  -30.89]  speed=20 wait=0.0s  (pre max|Δ|=8.72°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.66° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.3  -7.4  -1.4  +0.3  +8.7  -0.4  着いた
[SEND] pi05_chunk013_via09: send_angles (single) [ +40.84,  +48.49,   +3.21,   +2.38,  -55.01,  -30.98]  speed=20 wait=0.0s  (pre max|Δ|=3.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.20° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.0  -3.5  -1.3  +0.4  +4.0  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.20°（関節 j4）
[014] 推論 655ms  10ステップ先  Δ=-16.2 -23.5  -1.8  -2.1 +26.6  -0.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk014_via02: send_angles (single) [ +39.28,  +43.11,   +3.15,   +2.22,  -49.14,  -31.06]  speed=20 wait=0.0s  (pre max|Δ|=7.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.06° (tol 3.0°)
       [VIA   ] step  2  Δ= -1.6  -7.5  -1.3  +0.1  +7.6  -0.5  着いた
[SEND] pi05_chunk014_via05: send_angles (single) [ +33.70,  +36.92,   +2.96,   +1.38,  -41.75,  -30.98]  speed=20 wait=0.0s  (pre max|Δ|=9.05°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.83° (tol 3.0°)
       [VIA   ] step  5  Δ= -5.8  -8.3  -1.5  -1.4  +9.1  -0.3  着いた
[SEND] pi05_chunk014_via08: send_angles (single) [ +26.87,  +29.70,   +2.80,   +0.37,  -33.59,  -30.91]  speed=20 wait=0.0s  (pre max|Δ|=9.56°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.85° (tol 3.0°)
       [VIA   ] step  8  Δ= -6.8  -9.0  -1.7  -2.1  +9.6  -0.2  着いた
[SEND] pi05_chunk014_via09: send_angles (single) [ +24.70,  +27.10,   +2.72,   +0.03,  -30.62,  -30.88]  speed=20 wait=0.0s  (pre max|Δ|=4.45°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.67° (tol 3.0°)
       [VIA   ] step  9  Δ= -2.4  -4.4  -1.7  -1.5  +4.4  -0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.67°（関節 j2）
[015] 推論 611ms  10ステップ先  Δ=-21.1 -24.2  -2.1  -1.7 +27.5  -0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk015_via02: send_angles (single) [ +17.85,  +20.39,   +2.38,   -0.82,  -22.05,  -30.75]  speed=20 wait=0.0s  (pre max|Δ|=9.76°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.01° (tol 3.0°)
       [VIA   ] step  2  Δ= -6.9  -8.3  -2.0  -1.7  +9.8  -0.1  着いた
[SEND] pi05_chunk015_via05: send_angles (single) [ +10.74,  +12.19,   +2.36,   -0.89,  -12.67,  -30.76]  speed=20 wait=0.0s  (pre max|Δ|=10.35°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.03° (tol 3.0°)
       [VIA   ] step  5  Δ= -7.4  -9.9  -2.0  -0.9 +10.4  -0.1  着いた
[SEND] pi05_chunk015_via08: send_angles (single) [  +4.84,   +5.60,   +2.24,   -0.72,   -5.33,  -30.77]  speed=20 wait=0.0s  (pre max|Δ|=8.29°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.15° (tol 3.0°)
       [VIA   ] step  8  Δ= -6.1  -8.2  -2.2  -0.6  +8.3  -0.1  着いた
```

### 2回目 18:43 の端末の記録（失敗・806〜1013行目）

#### 2回目（pi05_20261106_184312）

```text
[SEND] pi05_chunk015_via09: send_angles (single) [  +3.65,   +4.49,   +2.24,   -0.73,   -4.62,  -30.75]  speed=20 wait=0.0s  (pre max|Δ|=2.54°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.54° (tol 3.0°)
       [VIA   ] step  9  Δ= -1.4  -2.5  -2.1  -0.3  +1.5  -0.1  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.15°（関節 j2）
[016] 推論 1461ms  10ステップ先  Δ= -2.6  -2.6  -2.2  -0.3  +1.6  -0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=5.97°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.40° (tol 2.5°)
[RECORD] 記録終了 コマ 1643 (8.548 fps) 実測角 1804 回 (9.39 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_183926/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > y
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_183926
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (199 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (2 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.17, 1.14, 1.40, -0.43, -0.26, -30.49]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=1.40°
[DONE] HOME 到達 max_err=1.40°
[POST] 最終姿勢: [-0.17, 1.14, 1.40, -0.43, -0.26, -30.49]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +1.9  dx=  -0.3  response=0.861  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (25 秒)

=== 8. 実機推論 (2 回目)
[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184312
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.6 dx=+6.9 response=0.549 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184312/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 625ms  10ステップ先  Δ= +3.7 +16.0 +11.7  -2.4 -22.8  +2.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.54,   +5.06,   +4.32,   -0.67,   -5.84,  -29.48]  speed=20 wait=0.0s  (pre max|Δ|=5.58°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.80° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.7  +3.9  +2.9  -0.4  -5.6  +0.8  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.65,  +10.36,   +8.10,   -1.52,  -13.28,  -28.55]  speed=20 wait=0.0s  (pre max|Δ|=9.24°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.86° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.9  +4.5  +2.8  -1.3  -9.2  +1.1  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +3.05,  +15.52,  +11.91,   -2.42,  -20.73,  -27.58]  speed=20 wait=0.0s  (pre max|Δ|=9.31°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.75° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.6  +4.3  +2.9  -2.1  -9.3  +1.2  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.50,  +17.10,  +13.05,   -2.69,  -23.04,  -27.30]  speed=20 wait=0.0s  (pre max|Δ|=4.06°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.51° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +0.7  +0.4  -1.2  -4.1  +0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.16°（関節 j4）
[001] 推論 1682ms  10ステップ先  Δ= +4.1 +15.4 +11.0  -1.3 -27.9  +1.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +4.69,  +21.83,  +16.35,   -3.29,  -30.54,  -26.70]  speed=20 wait=0.0s  (pre max|Δ|=8.66°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.93° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.3  +3.8  +2.6  -1.4  -8.7  +0.7  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +6.05,  +26.62,  +19.93,   -3.36,  -38.55,  -26.52]  speed=20 wait=0.0s  (pre max|Δ|=8.85°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.94° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.6  +3.9  +2.9  -0.4  -8.9  +0.4  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.49,  +31.94,  +23.78,   -3.21,  -47.42,  -26.44]  speed=20 wait=0.0s  (pre max|Δ|=9.72°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.93° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.7  +4.4  +3.3  -0.0  -9.7  +0.3  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +7.91,  +33.40,  +24.74,   -3.12,  -49.74,  -26.41]  speed=20 wait=0.0s  (pre max|Δ|=3.25°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.87° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.7  +0.6  +0.5  -0.1  -3.3  +0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.87°（関節 j1）
[002] 推論 732ms  10ステップ先  Δ= +3.1 +10.4  +7.7  -0.0 -16.0  +0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +9.49,  +38.90,  +28.46,   -3.32,  -57.95,  -26.39]  speed=20 wait=0.0s  (pre max|Δ|=8.91°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.82° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.7  +4.6  +3.3  -0.4  -8.9  +0.2  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [ +10.52,  +43.13,  +31.48,   -3.17,  -63.75,  -26.39]  speed=20 wait=0.0s  (pre max|Δ|=6.27°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.90° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.0  +3.4  +3.0  -0.2  -6.4  +0.2  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.87,  +44.74,  +32.65,   -2.94,  -65.20,  -26.37]  speed=20 wait=0.0s  (pre max|Δ|=2.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.10° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.3  +0.7  +1.0  -0.2  -2.1  +0.3  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.95,  +44.64,  +32.50,   -2.92,  -65.04,  -26.38]  speed=20 wait=0.0s  (pre max|Δ|=0.97°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.79° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.4  +0.6  +0.9  -0.2  -1.9  +0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.79°（関節 j1）
[003] 推論 1494ms  10ステップ先  Δ= +1.2  +8.4  -6.8  -0.4  +0.1  +0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53364]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd2c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b0688e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd3b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b0689d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd4d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd5c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd5e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53364]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b068d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11fcfd7c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +11.54 +45.43 +32.69 -2.63 -64.68 -26.54  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 1.0°）  届いた角度 +10.54 +45.43 +32.69 -2.63 -64.68 -26.54
[送信] j0 +1.0° → +12.54 +45.43 +32.69 -2.63 -64.68 -26.54  届いた（ずれ 0.44°）  届いた角度 +12.91 +45.43 +32.69 -3.07 -64.68 -26.54
[PAD   ] 補正 1 を終えました (13.2 秒, 送った目標 2 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 600ms  10ステップ先  Δ= +0.3 +10.5  -8.8  -0.1  +1.8  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +13.07,  +46.16,  +32.40,   -2.61,  -64.71,  -26.53]  speed=20 wait=0.0s  (pre max|Δ|=1.64°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.64° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.5  +0.7  -0.3  +0.0  +1.6  +0.0  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.09,  +50.04,  +29.26,   -2.69,  -64.23,  -26.60]  speed=20 wait=0.0s  (pre max|Δ|=3.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.12° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  +2.9  -3.4  -0.1  +2.1  -0.1  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.02,  +54.58,  +25.09,   -2.74,  -63.12,  -26.72]  speed=20 wait=0.0s  (pre max|Δ|=5.14°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.35° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  +3.7  -5.1  -0.1  +3.2  -0.2  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +12.88,  +55.88,  +23.90,   -2.74,  -62.87,  -26.76]  speed=20 wait=0.0s  (pre max|Δ|=2.60°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.90° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  +0.4  -2.1  -0.1  +2.6  -0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.90°（関節 j4）
[005] 推論 792ms  6ステップ先  Δ= +0.5  +3.0  -4.3  -0.1  +2.2  -0.5  グリッパー→閉
       [VIA   ] 中継点 2 点: step 2, 5
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.03,  +59.97,  +20.08,   -2.75,  -62.44,  -26.98]  speed=20 wait=0.0s  (pre max|Δ|=4.70°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.54° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.5  +3.1  -4.7  -0.2  +1.9  -0.4  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +13.06,  +59.79,  +20.54,   -2.72,  -62.56,  -27.01]  speed=20 wait=0.0s  (pre max|Δ|=1.42°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.42° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  -0.8  -0.2  -0.2  +1.4  -0.3  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 2.56°（関節 j4）
[006] 推論 1490ms  10ステップ先  Δ= +0.8  -9.8  +7.4  -1.0  +1.6  +0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +12.99,  +56.71,  +23.45,   -2.88,  -62.46,  -26.85]  speed=20 wait=0.0s  (pre max|Δ|=4.02°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.29° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.5  -4.0  +2.8  -0.8  +2.7  +0.5  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +13.00,  +53.04,  +26.66,   -3.01,  -62.70,  -26.67]  speed=20 wait=0.0s  (pre max|Δ|=4.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.54° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  -5.0  +3.9  -0.6  +0.5  +0.5  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +13.14,  +50.91,  +28.55,   -2.95,  -63.37,  -26.65]  speed=20 wait=0.0s  (pre max|Δ|=3.67°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.82° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.7  -3.7  +2.5  -0.2  -1.5  +0.2  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +13.32,  +50.91,  +28.15,   -2.86,  -63.50,  -26.78]  speed=20 wait=0.0s  (pre max|Δ|=1.73°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.73° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.7  -1.7  -0.2  -0.3  -1.6  -0.1  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.73°（関節 j1）
[007] 推論 557ms  10ステップ先  Δ=+16.5  +0.6 -12.1  +2.9  +1.5  -3.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk007_via02: send_angles (single) [ +16.00,  +50.16,  +26.99,   -2.35,  -63.02,  -27.19]  speed=20 wait=0.0s  (pre max|Δ|=3.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.13° (tol 3.0°)
       [VIA   ] step  2  Δ= +3.3  -2.5  -1.4  +0.2  -1.1  -0.6  着いた
[SEND] pi05_chunk007_via05: send_angles (single) [ +23.17,  +50.94,  +20.63,   -0.76,  -61.13,  -28.78]  speed=20 wait=0.0s  (pre max|Δ|=7.49°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.36° (tol 3.0°)
       [VIA   ] step  5  Δ= +7.2  -1.3  -7.5  +1.8  +0.7  -2.2  着いた
[SEND] pi05_chunk007_via08: send_angles (single) [ +28.12,  +52.39,  +16.92,   +0.21,  -60.30,  -29.77]  speed=20 wait=0.0s  (pre max|Δ|=5.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.21° (tol 3.0°)
       [VIA   ] step  8  Δ= +5.0  +0.1  -4.8  +1.5  +0.4  -2.3  着いた
[SEND] pi05_chunk007_via09: send_angles (single) [ +29.21,  +53.27,  +16.26,   +0.36,  -60.33,  -29.85]  speed=20 wait=0.0s  (pre max|Δ|=1.75°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.75° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.8  -0.2  -1.7  +0.8  -0.6  -1.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.75°（関節 j2）
[008] 推論 612ms  10ステップ先  Δ= +1.4  +0.1 -10.9  +1.5  +0.6  -1.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 8 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +54.22° → +53.87° まで（目標を 1 回送信。止めます）
[長押し] j1 を +42.18° → +38.40° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (12.7 秒, 送った目標 3 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[009] 推論 553ms  10ステップ先  Δ= +8.6 +14.6  -8.1  +1.9  -6.6  -0.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk009_via02: send_angles (single) [ +33.34,  +28.27,  +17.02,   +0.77,  -55.39,  -29.61]  speed=20 wait=0.0s  (pre max|Δ|=4.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.17° (tol 3.0°)
       [VIA   ] step  2  Δ= +4.4  +0.8  -1.9  +0.3  +0.9  -0.2  着いた
[SEND] pi05_chunk009_via05: send_angles (single) [ +35.73,  +33.37,  +15.16,   +1.30,  -58.58,  -29.69]  speed=20 wait=0.0s  (pre max|Δ|=3.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.96° (tol 3.0°)
       [VIA   ] step  5  Δ= +2.1  +3.9  -2.6  +0.9  -2.2  -0.3  着いた
[SEND] pi05_chunk009_via08: send_angles (single) [ +37.19,  +40.15,  +11.90,   +2.14,  -62.07,  -29.88]  speed=20 wait=0.0s  (pre max|Δ|=6.23°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.45° (tol 3.0°)
       [VIA   ] step  8  Δ= +3.4  +8.2  -5.8  +1.6  -5.6  -0.4  着いた
```

### 3回目 18:44 の端末の記録（成功・1014〜1265行目）

#### 3回目（pi05_20261106_184452）

```text
[SEND] pi05_chunk009_via09: send_angles (single) [ +37.50,  +42.13,  +10.75,   +2.31,  -62.79,  -29.97]  speed=20 wait=0.0s  (pre max|Δ|=2.60°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.64° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  +1.1  -2.6  +0.8  -1.5  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.55°（関節 j2）
[010] 推論 1702ms  10ステップ先  Δ= +4.6 +15.6  -7.5  +2.1  -8.5  -1.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=62.22°, post-send timeout 11.3s)
[SEND] arrival OK: max|Δ|=2.10° (tol 2.5°)
[RECORD] 記録終了 コマ 597 (8.099 fps) 実測角 644 回 (8.74 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184312/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > n
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184312
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (79 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (3 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.17, 1.49, 1.75, 0.35, -0.61, -29.70]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [-0.17, 1.49, 1.75, 0.35, -0.43, -29.70]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.2  dx=  -0.4  response=0.872  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (18 秒)

=== 8. 実機推論 (3 回目)

[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184452
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-3.9 dx=+6.5 response=0.536 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184452/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 612ms  10ステップ先  Δ= +2.9 +16.5 +11.4  -2.0 -21.5  +1.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.65,   +6.79,   +5.30,   +0.08,   -7.06,  -29.64]  speed=20 wait=0.0s  (pre max|Δ|=6.80°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.79° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  +5.3  +3.5  -0.2  -6.8  +0.1  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.36,  +11.36,   +8.64,   -0.36,  -13.44,  -29.36]  speed=20 wait=0.0s  (pre max|Δ|=8.08°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.84° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.6  +3.5  +2.3  -0.6  -8.1  +0.3  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +2.36,  +16.46,  +12.18,   -1.40,  -19.88,  -28.50]  speed=20 wait=0.0s  (pre max|Δ|=8.20°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.78° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.6  +4.1  +2.6  -1.2  -8.2  +1.2  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +2.78,  +18.03,  +13.15,   -1.70,  -21.76,  -28.25]  speed=20 wait=0.0s  (pre max|Δ|=3.66°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.29° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.1  +0.5  +0.1  -0.3  -3.7  +1.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.29°（関節 j4）
[001] 推論 552ms  10ステップ先  Δ= +4.5 +13.4  +8.1  -1.2 -22.5  +2.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +3.84,  +22.02,  +15.73,   -2.36,  -27.35,  -27.61]  speed=20 wait=0.0s  (pre max|Δ|=6.88°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.91° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.9  +3.0  +1.9  -0.8  -6.9  +1.4  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +5.38,  +26.42,  +18.43,   -2.62,  -34.11,  -27.29]  speed=20 wait=0.0s  (pre max|Δ|=7.57°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.91° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.7  +3.5  +1.9  -0.1  -7.6  +1.0  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +6.96,  +31.04,  +21.27,   -2.78,  -41.11,  -27.10]  speed=20 wait=0.0s  (pre max|Δ|=7.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.21° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.7  +3.7  +2.1  +0.3  -7.6  +0.8  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +7.44,  +32.42,  +22.01,   -2.79,  -43.01,  -27.07]  speed=20 wait=0.0s  (pre max|Δ|=3.11°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.80° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +0.4  +0.2  +0.5  -3.1  +0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.80°（関節 j1）
[002] 推論 598ms  10ステップ先  Δ= +2.7 +13.7  +7.6  +1.1 -20.0  +0.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +8.50,  +36.57,  +24.63,   -2.79,  -48.98,  -26.95]  speed=20 wait=0.0s  (pre max|Δ|=6.45°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.87° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.1  +3.4  +2.8  +0.5  -6.4  +0.5  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [  +9.56,  +41.51,  +27.27,   -2.47,  -55.80,  -26.86]  speed=20 wait=0.0s  (pre max|Δ|=7.55°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.85° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.9  +4.1  +2.2  +0.7  -7.6  +0.5  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.16,  +45.77,  +29.29,   -2.45,  -61.46,  -26.77]  speed=20 wait=0.0s  (pre max|Δ|=6.36°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.98° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  +3.4  +1.9  +0.5  -6.4  +0.5  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.27,  +46.97,  +29.39,   -2.46,  -62.51,  -26.75]  speed=20 wait=0.0s  (pre max|Δ|=1.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.96° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  +0.2  +0.1  +0.4  -2.0  +0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.90°（関節 j4）
[003] 推論 556ms  10ステップ先  Δ= +3.1  +1.8  +0.3  +0.3  -1.8  +0.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53388]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0c890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf52c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0c8e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0c958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0c980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf53b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0c9d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0ca20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0ca98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf54d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cb38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cb88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf55c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cbb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf55e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cbd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cc50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0ccc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cd18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cd40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf5778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53388]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x11bc0cd90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12edf57c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 -1.0° → +8.66 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 0.36°）  届いた角度 +8.96 +47.90 +29.26 -2.54 -61.96 -26.98
[止める] stop（released_while_sending）  止まった角度 +9.75 +48.86 +29.26 -2.63 -60.46 -27.07
[長押し] j0 を +8.96° → +16.34° まで（目標を 1 回送信。止めます）
[送信] j0 -1.0° → +21.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 2.99°）  届いた角度 +21.18 +49.74 +29.26 -1.23 -58.97 -27.15
[送信] j0 -1.0° → +20.14 +47.90 +29.26 -2.90 -61.96 -27.07  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 1.85°）  届いた角度 +21.09 +48.77 +29.26 -1.84 -60.11 -27.15
[送信] j0 -1.0° → +19.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 0.88°）  届いた角度 +18.72 +48.77 +29.26 -2.02 -61.25 -27.15
[送信] j0 -1.0° → +18.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 0.87°）  届いた角度 +18.28 +48.77 +29.26 -2.19 -61.34 -27.15
[送信] j0 -1.0° → +17.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 0.87°）  届いた角度 +17.31 +48.77 +29.26 -2.10 -61.34 -27.24
[送信] j0 -1.0° → +16.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 0.87°）  届いた角度 +16.17 +48.77 +29.26 -2.10 -61.34 -27.15
[送信] j0 -1.0° → +15.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 0.87°）  届いた角度 +15.29 +48.77 +29.26 -2.19 -62.57 -27.15
[送信] j0 -2.0° → +13.14 +47.90 +29.26 -2.90 -61.96 -27.07  届いた（ずれ 1.06°）  届いた角度 +12.83 +48.77 +29.26 -1.84 -62.57 -27.07
[PAD   ] 補正 1 を終えました (20.0 秒, 送った目標 11 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 1507ms  7ステップ先  Δ= +1.0  +7.9  -5.4  -0.1  -1.1  -0.3  グリッパー→閉
       [VIA   ] 中継点 3 点: step 2, 5, 6
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +13.70,  +53.67,  +26.32,   -2.16,  -63.12,  -27.21]  speed=20 wait=0.0s  (pre max|Δ|=4.90°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.10° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  +4.9  -2.9  -0.1  -0.6  -0.2  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.95,  +56.72,  +23.85,   -2.16,  -63.70,  -27.37]  speed=20 wait=0.0s  (pre max|Δ|=3.57°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.93° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.2  +2.1  -3.6  -0.1  -1.1  -0.4  着いた
[SEND] pi05_chunk004_via06: send_angles (single) [ +13.95,  +56.72,  +23.85,   -2.16,  -63.70,  -27.37]  speed=20 wait=0.0s  (pre max|Δ|=0.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.93° (tol 3.0°)
       [VIA   ] step  6  Δ= +0.3  -0.8  -0.9  -0.1  +0.5  -0.4  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.95°（関節 j4）
[005] 推論 556ms  10ステップ先  Δ= +9.1  -5.8  -5.1  +1.0  +6.1  -1.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +14.50,  +51.70,  +27.49,   -2.40,  -62.61,  -27.09]  speed=20 wait=0.0s  (pre max|Δ|=5.86°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.91° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.1  -5.9  +2.7  -0.6  +3.2  +0.2  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +15.20,  +50.45,  +27.29,   -2.35,  -62.63,  -27.08]  speed=20 wait=0.0s  (pre max|Δ|=3.16°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.93° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.7  -3.2  +0.1  +0.0  +0.0  +0.1  着いた
[SEND] pi05_chunk005_via08: send_angles (single) [ +20.54,  +52.03,  +21.40,   -1.23,  -60.42,  -28.44]  speed=20 wait=0.0s  (pre max|Δ|=6.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.29° (tol 3.0°)
       [VIA   ] step  8  Δ= +5.5  -0.3  -6.1  +1.1  +1.7  -1.3  着いた
[SEND] pi05_chunk005_via09: send_angles (single) [ +22.45,  +51.72,  +19.64,   -0.79,  -59.54,  -28.85]  speed=20 wait=0.0s  (pre max|Δ|=2.77°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.89° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.9  -1.1  -2.8  +1.0  +0.3  -1.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.26°（関節 j5）
[006] 推論 833ms  10ステップ先  Δ= -0.2  -1.4  -7.1  +0.8  -0.0  -1.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 6 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +52.82° → +45.17° まで（目標を 1 回送信。止めます）
[長押し] j1 を +39.55° → +34.27° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (11.1 秒, 送った目標 3 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[007] 推論 1730ms  10ステップ先  Δ=+10.2 +12.4  -3.2  +1.3  -8.9  -0.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk007_via02: send_angles (single) [ +28.99,  +27.93,  +21.24,   -0.28,  -54.56,  -29.00]  speed=20 wait=0.0s  (pre max|Δ|=6.58°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.33° (tol 3.0°)
       [VIA   ] step  2  Δ= +6.6  +0.3  -0.7  +0.8  +1.1  -0.4  着いた
[SEND] pi05_chunk007_via05: send_angles (single) [ +31.88,  +31.73,  +21.36,   +0.02,  -57.64,  -29.25]  speed=20 wait=0.0s  (pre max|Δ|=2.82°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.88° (tol 3.0°)
       [VIA   ] step  5  Δ= +2.4  +2.8  -0.4  +1.1  -1.7  -0.7  着いた
[SEND] pi05_chunk007_via08: send_angles (single) [ +32.54,  +38.04,  +19.84,   +0.15,  -62.85,  -29.34]  speed=20 wait=0.0s  (pre max|Δ|=6.60°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.95° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  +5.4  -2.0  +0.6  -6.6  -0.8  着いた
[SEND] pi05_chunk007_via09: send_angles (single) [ +32.66,  +39.97,  +18.80,   +0.21,  -64.06,  -29.41]  speed=20 wait=0.0s  (pre max|Δ|=2.99°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.99° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  +0.9  -3.0  +0.5  -2.1  -0.8  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.99°（関節 j2）
[008] 推論 618ms  10ステップ先  Δ=+10.5 +14.1 -14.7  +2.7  -2.4  -3.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk008_via02: send_angles (single) [ +39.63,  +41.81,  +12.00,   +1.33,  -61.71,  -30.56]  speed=20 wait=0.0s  (pre max|Δ|=8.21°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.88° (tol 3.0°)
       [VIA   ] step  2  Δ= +7.3  +0.9  -8.2  +1.3  +4.1  -1.5  着いた
[SEND] pi05_chunk008_via05: send_angles (single) [ +42.88,  +46.31,   +7.06,   +2.02,  -60.70,  -31.37]  speed=20 wait=0.0s  (pre max|Δ|=6.21°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.40° (tol 3.0°)
       [VIA   ] step  5  Δ= +3.3  +3.7  -6.2  +0.2  +3.9  -1.1  着いた
[SEND] pi05_chunk008_via08: send_angles (single) [ +43.12,  +52.19,   +6.67,   +2.40,  -63.55,  -31.56]  speed=20 wait=0.0s  (pre max|Δ|=4.82°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.50° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.2  +4.8  -1.6  +0.2  -0.4  -0.5  着いた
[SEND] pi05_chunk008_via09: send_angles (single) [ +42.80,  +53.27,   +7.09,   +2.45,  -64.37,  -31.58]  speed=20 wait=0.0s  (pre max|Δ|=1.27°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.27° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.1  +0.0  -1.1  +0.3  -1.3  -0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.08°（関節 j2）
[009] 推論 628ms  9ステップ先  Δ= +0.5  +8.4  -3.5  +0.8  -6.6  -0.6  グリッパー→開
       [VIA   ] 中継点 3 点: step 2, 5, 8
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk009_via02: send_angles (single) [ +43.18,  +58.03,   +5.28,   +2.83,  -66.55,  -31.86]  speed=20 wait=0.0s  (pre max|Δ|=3.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.31° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.3  +3.6  -2.9  +0.6  -2.5  -0.6  着いた
[SEND] pi05_chunk009_via05: send_angles (single) [ +43.11,  +62.26,   +4.47,   +3.00,  -69.32,  -31.94]  speed=20 wait=0.0s  (pre max|Δ|=3.85°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.94° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.2  +3.2  -2.1  +0.4  -3.8  -0.4  着いた
[SEND] pi05_chunk009_via08: send_angles (single) [ +43.40,  +62.09,   +4.70,   +3.01,  -70.01,  -31.91]  speed=20 wait=0.0s  (pre max|Δ|=1.64°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.55° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  -0.9  -1.2  +0.3  -1.6  -0.3  着いた
  [Pi] GRIPPER: OPEN result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.19°（関節 j1）
[010] 推論 566ms  10ステップ先  Δ= -0.9 -15.2  -0.4  +0.4 +13.7  -0.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk010_via02: send_angles (single) [ +43.22,  +56.98,   +4.94,   +3.08,  -64.78,  -32.06]  speed=20 wait=0.0s  (pre max|Δ|=6.30°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.98° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.3  -6.3  -0.9  +0.3  +4.7  -0.4  着いた
[SEND] pi05_chunk010_via05: send_angles (single) [ +43.01,  +52.40,   +5.11,   +3.15,  -60.11,  -32.13]  speed=20 wait=0.0s  (pre max|Δ|=7.65°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.73° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.1  -6.7  -0.8  +0.3  +7.6  -0.4  着いた
[SEND] pi05_chunk010_via08: send_angles (single) [ +42.24,  +48.92,   +5.40,   +3.21,  -56.31,  -32.16]  speed=20 wait=0.0s  (pre max|Δ|=6.53°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.57° (tol 3.0°)
       [VIA   ] step  8  Δ= -0.7  -5.6  -0.5  +0.3  +6.5  -0.4  着いた
[SEND] pi05_chunk010_via09: send_angles (single) [ +41.95,  +48.11,   +5.51,   +3.22,  -55.78,  -32.17]  speed=20 wait=0.0s  (pre max|Δ|=3.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.60° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.6  -2.7  -0.4  +0.2  +3.1  -0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.60°（関節 j1）

[011] 推論 1469ms  10ステップ先  Δ=-17.4 -24.4  -1.0  -2.3 +28.3  +0.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk011_via02: send_angles (single) [ +38.64,  +42.81,   +5.32,   +2.92,  -49.49,  -32.05]  speed=20 wait=0.0s  (pre max|Δ|=7.90°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.10° (tol 3.0°)
       [VIA   ] step  2  Δ= -3.9  -7.9  -0.6  -0.1  +7.7  -0.2  着いた
[SEND] pi05_chunk011_via05: send_angles (single) [ +33.00,  +36.89,   +4.98,   +2.01,  -41.99,  -31.72]  speed=20 wait=0.0s  (pre max|Δ|=8.72°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.95° (tol 3.0°)
       [VIA   ] step  5  Δ= -5.7  -8.0  -0.9  -1.6  +8.7  +0.2  着いた
[SEND] pi05_chunk011_via08: send_angles (single) [ +27.02,  +29.12,   +4.83,   +1.01,  -33.16,  -31.38]  speed=20 wait=0.0s  (pre max|Δ|=9.99°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.90° (tol 3.0°)
       [VIA   ] step  8  Δ= -6.0  -9.6  -1.1  -2.2 +10.0  +0.5  着いた
```

### 4回目 18:46 の端末の記録（成功・1266〜1541行目）

#### 4回目（pi05_20261106_184650）

```text
[SEND] pi05_chunk011_via09: send_angles (single) [ +25.08,  +26.35,   +4.86,   +0.71,  -29.83,  -31.27]  speed=20 wait=0.0s  (pre max|Δ|=4.67°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.74° (tol 3.0°)
       [VIA   ] step  9  Δ= -2.2  -4.7  -1.0  -1.4  +4.4  +0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.77°（関節 j1）
[012] 推論 550ms  10ステップ先  Δ=-22.0 -23.5  -1.7  -1.3 +28.3  +0.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=30.67°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.75° (tol 2.5°)
[RECORD] 記録終了 コマ 757 (8.238 fps) 実測角 800 回 (8.71 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184452/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > y
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184452
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (98 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (4 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.17, 1.40, 1.75, 0.35, -0.35, -30.32]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [-0.17, 1.40, 1.75, 0.35, -0.26, -30.32]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.2  dx=  -0.2  response=0.921  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (19 秒)

=== 8. 実機推論 (4 回目)

[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184650
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.0 dx=+6.5 response=0.524 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184650/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 741ms  10ステップ先  Δ= +3.8 +16.1 +12.2  -2.3 -24.5  +2.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.62,   +6.05,   +4.85,   -0.12,   -6.80,  -29.62]  speed=20 wait=0.0s  (pre max|Δ|=6.54°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.97° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.9  +4.7  +3.1  -0.4  -6.5  +0.6  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.73,  +11.02,   +8.65,   -0.84,  -14.40,  -28.73]  speed=20 wait=0.0s  (pre max|Δ|=9.57°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.66° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.0  +4.1  +2.8  -1.0  -9.6  +1.1  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +3.11,  +15.73,  +12.54,   -1.77,  -21.86,  -27.73]  speed=20 wait=0.0s  (pre max|Δ|=9.12°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.83° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.0  +3.8  +3.1  -1.0  -9.1  +1.2  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.66,  +17.54,  +13.94,   -2.08,  -24.76,  -27.38]  speed=20 wait=0.0s  (pre max|Δ|=4.73°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.39° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +0.9  +0.6  -0.2  -4.7  +0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.39°（関節 j4）
[001] 推論 1475ms  10ステップ先  Δ= +4.2 +15.0  +9.8  -0.4 -24.8  +0.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +4.80,  +21.97,  +16.58,   -2.69,  -30.90,  -26.93]  speed=20 wait=0.0s  (pre max|Δ|=7.53°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.88° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.1  +3.6  +2.0  -0.4  -7.5  +0.7  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +6.01,  +26.73,  +19.71,   -2.76,  -37.95,  -26.75]  speed=20 wait=0.0s  (pre max|Δ|=7.81°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.95° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.1  +3.9  +2.4  +0.2  -7.8  +0.4  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.44,  +31.79,  +23.23,   -2.65,  -45.76,  -26.66]  speed=20 wait=0.0s  (pre max|Δ|=8.76°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.81° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.6  +4.2  +2.8  +0.8  -8.8  +0.3  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +7.93,  +33.36,  +24.36,   -2.64,  -48.18,  -26.64]  speed=20 wait=0.0s  (pre max|Δ|=3.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.72° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  +0.8  +0.6  +0.7  -3.1  +0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.91°（関節 j1）
[002] 推論 559ms  10ステップ先  Δ= +3.5  +9.6  +6.2  +0.1 -14.5  +0.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +9.45,  +38.47,  +27.71,   -2.70,  -55.33,  -26.60]  speed=20 wait=0.0s  (pre max|Δ|=7.79°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.90° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.5  +4.2  +2.9  +0.5  -7.8  +0.3  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [ +10.71,  +43.20,  +30.37,   -2.91,  -61.24,  -26.53]  speed=20 wait=0.0s  (pre max|Δ|=6.40°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.86° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.4  +3.8  +2.3  +0.1  -6.4  +0.3  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.89,  +44.53,  +30.83,   -2.89,  -62.39,  -26.51]  speed=20 wait=0.0s  (pre max|Δ|=1.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.66° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.1  +0.5  +0.3  +0.1  -1.9  +0.2  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.93,  +43.92,  +30.39,   -2.96,  -62.04,  -26.51]  speed=20 wait=0.0s  (pre max|Δ|=1.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.51° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  -1.2  -0.1  -0.2  -0.1  +0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.51°（関節 j1）
[003] 推論 594ms  10ステップ先  Δ= +1.5  +9.1  -3.8  -0.3  -1.9  +0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53422]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd2c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d48e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd3b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d49d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd4d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd5c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd5e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53422]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10a8d4d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x11e9fd7c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +11.72 +45.43 +30.49 -2.81 -62.13 -26.71  届いた（ずれ 0.18°）  届いた角度 +11.86 +45.43 +30.49 -2.63 -62.13 -26.71
[PAD   ] 補正 1 を終えました (9.8 秒, 送った目標 1 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 552ms  10ステップ先  Δ= +1.1 +12.4  -8.9  -0.3  -0.9  -0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +12.02,  +48.21,  +29.65,   -2.80,  -62.99,  -26.64]  speed=20 wait=0.0s  (pre max|Δ|=2.78°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.78° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.1  +2.8  -0.8  -0.1  -0.9  +0.1  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +12.51,  +53.30,  +26.05,   -2.90,  -63.63,  -26.61]  speed=20 wait=0.0s  (pre max|Δ|=4.44°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.02° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.6  +4.2  -4.4  -0.2  -1.1  +0.1  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +12.91,  +57.23,  +22.27,   -3.00,  -63.02,  -26.72]  speed=20 wait=0.0s  (pre max|Δ|=4.80°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.86° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.0  +3.1  -4.8  -0.4  -0.2  -0.0  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +13.02,  +57.82,  +21.61,   -3.02,  -63.05,  -26.78]  speed=20 wait=0.0s  (pre max|Δ|=1.50°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.41° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  -0.3  -1.5  -0.5  -0.2  -0.1  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.24°（関節 j2）
[005] 推論 775ms  6ステップ先  Δ= +0.4  +1.9  -2.8  -0.3  -0.1  +0.0  グリッパー→閉
       [VIA   ] 中継点 2 点: step 2, 5
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.28,  +60.14,  +20.41,   -3.00,  -63.75,  -26.66]  speed=20 wait=0.0s  (pre max|Δ|=2.44°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.37° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  +1.6  -2.4  -0.4  -0.1  +0.0  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +13.26,  +60.46,  +20.02,   -2.92,  -63.78,  -26.67]  speed=20 wait=0.0s  (pre max|Δ|=1.34°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.34° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.4  -0.4  -1.2  -0.3  +1.3  +0.1  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 2.84°（関節 j4）
[006] 推論 1504ms  10ステップ先  Δ= +0.4  -7.1  +4.3  -0.9  +2.8  +0.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +13.37,  +59.93,  +20.07,   -2.93,  -63.88,  -26.72]  speed=20 wait=0.0s  (pre max|Δ|=2.74°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.74° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  -1.2  -1.0  -0.4  +2.7  +0.3  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +13.17,  +57.59,  +22.37,   -3.08,  -63.80,  -26.43]  speed=20 wait=0.0s  (pre max|Δ|=3.49°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.47° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  -3.5  +1.3  -0.5  +2.8  +0.5  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +13.07,  +54.49,  +25.11,   -3.26,  -63.74,  -26.20]  speed=20 wait=0.0s  (pre max|Δ|=4.57°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.49° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.2  -4.6  +3.6  -0.6  -0.0  +0.7  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +12.97,  +54.01,  +25.35,   -3.27,  -63.83,  -26.19]  speed=20 wait=0.0s  (pre max|Δ|=1.97°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.70° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.1  -2.0  +0.7  -0.1  -1.3  +0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.70°（関節 j4）
[007] 推論 571ms  10ステップ先  Δ= +0.3  -1.0  -6.3  -0.1  -1.7  +0.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 7 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +55.63° → +46.05° まで（目標を 1 回送信。止めます）
[長押し] j1 を +40.95° → +27.68° まで（目標を 2 回送信。止めます）
[関節] 上下で動かす関節を j2 にしました
[長押し] j2 を +26.45° → +28.91° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (26.6 秒, 送った目標 6 回). この姿勢から推論に戻ります
[PAD   ] [注意] 学習データの範囲の外にいます: j2=+42.5 (範囲 -46〜+41). 次の推論は範囲の端へ切られるか, 止まります
[STATE ] 実測を渡す (直前に送った目標がない)
[008] 推論 774ms  10ステップ先  Δ= +6.1 +14.0 -12.5  +2.7  -1.3  -1.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > r
       爪を締め直します (開かずに閉じる指令をもう一度)
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk008_via02: send_angles (single) [ +13.06,  +13.91,  +41.04,   -2.68,  -64.49,  -26.41]  speed=20 wait=0.0s  (pre max|Δ|=2.05°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.05° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.2  +2.1  -1.4  +0.7  -0.2  -0.1  着いた
[SEND] pi05_chunk008_via05: send_angles (single) [ +14.06,  +18.84,  +37.45,   -1.79,  -65.70,  -26.92]  speed=20 wait=0.0s  (pre max|Δ|=7.42°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.57° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.1  +7.2  -5.0  +1.6  -1.4  -0.6  着いた
[SEND] pi05_chunk008_via08: send_angles (single) [ +17.57,  +24.29,  +31.80,   -0.96,  -65.06,  -27.73]  speed=20 wait=0.0s  (pre max|Δ|=7.22°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.42° (tol 3.0°)
       [VIA   ] step  8  Δ= +3.6  +4.6  -7.2  +1.4  +0.4  -1.4  着いた
[SEND] pi05_chunk008_via09: send_angles (single) [ +18.98,  +25.85,  +29.97,   -0.76,  -64.49,  -27.95]  speed=20 wait=0.0s  (pre max|Δ|=3.25°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.49° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.1  +0.6  -3.3  +0.8  +1.0  -1.0  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.49°（関節 j2）
[009] 推論 1523ms  10ステップ先  Δ=+14.6 +11.2 -18.3  +3.8  +2.6  -3.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk009_via02: send_angles (single) [ +25.74,  +26.74,  +24.61,   +0.71,  -62.23,  -29.27]  speed=20 wait=0.0s  (pre max|Δ|=6.94°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.19° (tol 3.0°)
       [VIA   ] step  2  Δ= +6.9  -0.1  -6.9  +1.8  +3.2  -1.9  着いた
[SEND] pi05_chunk009_via05: send_angles (single) [ +30.66,  +28.64,  +18.82,   +1.85,  -60.80,  -30.39]  speed=20 wait=0.0s  (pre max|Δ|=7.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.04° (tol 3.0°)
       [VIA   ] step  5  Δ= +5.0  +1.0  -7.1  +1.4  +3.5  -1.6  着いた
[SEND] pi05_chunk009_via08: send_angles (single) [ +32.91,  +35.43,  +14.28,   +2.60,  -62.30,  -30.97]  speed=20 wait=0.0s  (pre max|Δ|=5.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.54° (tol 3.0°)
       [VIA   ] step  8  Δ= +2.2  +5.9  -5.9  +1.2  +0.4  -1.0  着いた
[SEND] pi05_chunk009_via09: send_angles (single) [ +33.37,  +37.97,  +13.12,   +2.78,  -62.86,  -31.02]  speed=20 wait=0.0s  (pre max|Δ|=2.70°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.34° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.4  +1.5  -2.7  +0.4  -0.1  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.82°（関節 j2）
[010] 推論 596ms  10ステップ先  Δ= +9.0 +15.1  -7.3  +1.7  -7.5  -1.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk010_via02: send_angles (single) [ +40.67,  +39.36,  +10.36,   +3.28,  -61.97,  -31.38]  speed=20 wait=0.0s  (pre max|Δ|=7.72°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.50° (tol 3.0°)
       [VIA   ] step  2  Δ= +7.7  +0.3  -4.6  +0.9  +0.8  -0.8  着いた
[SEND] pi05_chunk010_via05: send_angles (single) [ +41.63,  +45.92,   +9.24,   +3.44,  -65.52,  -31.40]  speed=20 wait=0.0s  (pre max|Δ|=5.76°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.57° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.8  +5.8  -2.6  +0.5  -2.8  -0.4  着いた
[SEND] pi05_chunk010_via08: send_angles (single) [ +42.02,  +52.54,   +8.10,   +3.85,  -69.56,  -31.45]  speed=20 wait=0.0s  (pre max|Δ|=5.52°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.48° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.2  +5.5  -2.7  +0.3  -5.1  -0.4  着いた
[SEND] pi05_chunk010_via09: send_angles (single) [ +41.94,  +54.26,   +7.62,   +3.97,  -70.26,  -31.48]  speed=20 wait=0.0s  (pre max|Δ|=1.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.96° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  +0.6  -2.0  +0.3  -1.7  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.96°（関節 j2）
[011] 推論 556ms  7ステップ先  Δ= +0.8  +7.9  -5.0  +0.7  -5.3  -0.8  グリッパー→開
       [VIA   ] 中継点 3 点: step 2, 5, 6
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk011_via02: send_angles (single) [ +42.20,  +59.75,   +5.31,   +4.27,  -72.73,  -31.67]  speed=20 wait=0.0s  (pre max|Δ|=4.30°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.19° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.5  +4.3  -3.7  +0.4  -3.0  -0.6  着いた
[SEND] pi05_chunk011_via05: send_angles (single) [ +42.50,  +61.63,   +4.59,   +4.45,  -73.84,  -31.79]  speed=20 wait=0.0s  (pre max|Δ|=2.13°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.13° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.8  +0.7  -1.9  +0.5  -2.1  -0.4  着いた
[SEND] pi05_chunk011_via06: send_angles (single) [ +42.50,  +61.63,   +4.59,   +4.45,  -73.84,  -31.79]  speed=20 wait=0.0s  (pre max|Δ|=1.86°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.47° (tol 3.0°)
       [VIA   ] step  6  Δ= +0.8  +0.7  -1.9  +0.5  -2.1  -0.4  着いた
  [Pi] GRIPPER: OPEN result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.29°（関節 j2）
[012] 推論 818ms  10ステップ先  Δ= -1.6 -17.2  -0.1  +0.0 +15.4  -0.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk012_via02: send_angles (single) [ +42.48,  +56.27,   +4.96,   +4.46,  -68.47,  -31.94]  speed=20 wait=0.0s  (pre max|Δ|=6.57°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.89° (tol 3.0°)
       [VIA   ] step  2  Δ= -0.1  -6.6  -0.9  +0.1  +4.6  -0.5  着いた
[SEND] pi05_chunk012_via05: send_angles (single) [ +42.41,  +51.33,   +5.22,   +4.45,  -63.42,  -32.02]  speed=20 wait=0.0s  (pre max|Δ|=7.85°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.41° (tol 3.0°)
       [VIA   ] step  5  Δ= -0.1  -6.9  -0.7  -0.0  +7.8  -0.4  着いた
[SEND] pi05_chunk012_via08: send_angles (single) [ +41.40,  +46.77,   +5.64,   +4.36,  -58.87,  -32.10]  speed=20 wait=0.0s  (pre max|Δ|=6.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.30° (tol 3.0°)
       [VIA   ] step  8  Δ= -1.1  -6.5  -0.2  -0.2  +7.0  -0.4  着いた
[SEND] pi05_chunk012_via09: send_angles (single) [ +40.98,  +45.65,   +5.74,   +4.31,  -57.74,  -32.12]  speed=20 wait=0.0s  (pre max|Δ|=3.43°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.16° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.2  -3.1  -0.2  -0.4  +3.4  -0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.16°（関節 j1）
[013] 推論 1541ms  10ステップ先  Δ=-19.2 -25.3  -1.0  -3.6 +30.0  +0.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk013_via02: send_angles (single) [ +35.87,  +39.20,   +5.48,   +3.55,  -50.58,  -31.88]  speed=20 wait=0.0s  (pre max|Δ|=8.61°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.02° (tol 3.0°)
       [VIA   ] step  2  Δ= -5.3  -8.6  -0.7  -1.3  +8.6  -0.1  着いた
[SEND] pi05_chunk013_via05: send_angles (single) [ +30.17,  +32.25,   +5.21,   +2.55,  -42.23,  -31.54]  speed=20 wait=0.0s  (pre max|Δ|=9.53°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.11° (tol 3.0°)
       [VIA   ] step  5  Δ= -5.9  -9.0  -0.9  -1.9  +9.5  +0.3  着いた
[SEND] pi05_chunk013_via08: send_angles (single) [ +24.15,  +24.95,   +4.92,   +1.56,  -32.77,  -31.24]  speed=20 wait=0.0s  (pre max|Δ|=10.73°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.85° (tol 3.0°)
       [VIA   ] step  8  Δ= -6.0  -9.4  -1.2  -1.9 +10.7  +0.7  着いた
```

### 5回目 18:49 の端末の記録（成功・1542〜1790行目）

#### 5回目（pi05_20261106_184906）

```text
[SEND] pi05_chunk013_via09: send_angles (single) [ +21.96,  +22.55,   +5.07,   +1.27,  -29.76,  -31.15]  speed=20 wait=0.0s  (pre max|Δ|=4.25°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.79° (tol 3.0°)
       [VIA   ] step  9  Δ= -2.7  -4.3  -1.1  -1.1  +4.1  +0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.79°（関節 j1）
[014] 推論 829ms  10ステップ先  Δ=-14.3 -16.0  -2.7  -2.3 +20.2  +0.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=30.58°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.75° (tol 2.5°)
[RECORD] 記録終了 コマ 894 (8.295 fps) 実測角 957 回 (8.88 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184650/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > y
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184650
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (119 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (5 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.35, 1.49, 1.75, 0.26, -0.26, -30.23]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [-0.35, 1.49, 1.75, 0.26, -0.26, -30.23]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.2  dx=  -0.8  response=0.918  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (14 秒)

=== 8. 実機推論 (5 回目)

[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184906
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.4 dx=+6.3 response=0.572 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184906/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 1753ms  10ステップ先  Δ= +4.0 +17.4 +11.9  -2.4 -22.7  +3.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.51,   +6.90,   +5.45,   -0.06,   -7.20,  -29.54]  speed=20 wait=0.0s  (pre max|Δ|=6.94°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.37° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.9  +5.4  +3.7  -0.3  -6.9  +0.7  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.45,  +12.05,   +9.08,   -0.81,  -14.03,  -28.61]  speed=20 wait=0.0s  (pre max|Δ|=9.11°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.91° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.8  +4.1  +2.7  -1.1  -9.1  +1.1  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +2.81,  +16.95,  +12.56,   -1.91,  -20.92,  -27.58]  speed=20 wait=0.0s  (pre max|Δ|=8.80°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.85° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.0  +3.9  +2.6  -1.4  -8.8  +1.2  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.57,  +18.85,  +13.65,   -2.18,  -23.01,  -27.28]  speed=20 wait=0.0s  (pre max|Δ|=3.94°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.31° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.8  +0.9  +0.3  -0.2  -3.9  +0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.22°（関節 j4）
[001] 推論 613ms  10ステップ先  Δ= +4.1 +13.8  +9.5  -1.0 -24.2  +1.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +4.94,  +23.54,  +16.64,   -2.86,  -30.02,  -26.75]  speed=20 wait=0.0s  (pre max|Δ|=8.23°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.37° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.4  +3.7  +2.2  -0.4  -8.2  +0.8  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +6.31,  +28.10,  +19.84,   -2.85,  -37.49,  -26.65]  speed=20 wait=0.0s  (pre max|Δ|=8.84°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.64° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.5  +3.7  +2.4  +0.3  -8.8  +0.4  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.52,  +32.30,  +22.60,   -3.01,  -44.31,  -26.54]  speed=20 wait=0.0s  (pre max|Δ|=8.37°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.34° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.5  +3.3  +2.1  +0.2  -8.4  +0.4  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +7.82,  +33.60,  +23.32,   -3.04,  -45.97,  -26.52]  speed=20 wait=0.0s  (pre max|Δ|=3.00°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.82° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.4  +0.5  +0.1  +0.1  -3.0  +0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.33°（関節 j4）
[002] 推論 857ms  10ステップ先  Δ= +3.0 +13.1  +8.4  +0.5 -19.5  +0.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +9.35,  +40.02,  +27.64,   -3.11,  -55.84,  -26.42]  speed=20 wait=0.0s  (pre max|Δ|=10.58°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.91° (tol 3.0°)
       [VIA   ] step  2  Δ= +2.0  +5.6  +4.4  +0.1 -10.6  +0.3  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [ +10.24,  +45.16,  +30.70,   -2.91,  -62.61,  -26.37]  speed=20 wait=0.0s  (pre max|Δ|=7.68°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.92° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.4  +4.3  +3.0  +0.3  -7.7  +0.3  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.36,  +46.75,  +31.58,   -2.67,  -64.05,  -26.33]  speed=20 wait=0.0s  (pre max|Δ|=2.36°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.36° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.2  +0.7  +0.9  +0.5  -2.4  +0.2  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.34,  +47.09,  +31.58,   -2.66,  -64.17,  -26.33]  speed=20 wait=0.0s  (pre max|Δ|=0.74°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.63° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  +0.2  +0.9  +0.5  -1.0  +0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.72°（関節 j1）
[003] 推論 1557ms  10ステップ先  Δ= +0.9  +8.7  -7.9  +0.5  +0.3  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53459]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d2c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f88e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d3b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f89d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d4d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d5c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d5e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53459]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1254f8d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x13724d7c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +11.19 +47.81 +31.81 -2.98 -63.54 -26.54  届いた（ずれ 1.41°）  届いた角度 +11.51 +47.81 +31.81 -2.98 -64.95 -26.54
[送信] j0 +1.0° → +12.19 +47.81 +31.81 -2.98 -63.54 -26.54  届いた（ずれ 1.41°）  届いた角度 +12.21 +47.81 +31.81 -2.72 -64.95 -26.54
[PAD   ] 補正 1 を終えました (9.4 秒, 送った目標 2 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 549ms  10ステップ先  Δ= +1.3 +11.0  -8.8  -0.0  +2.0  -0.4  グリッパー→閉
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +13.07,  +53.39,  +27.65,   -2.72,  -64.25,  -26.52]  speed=20 wait=0.0s  (pre max|Δ|=5.58°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.00° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  +5.6  -4.2  -0.1  +0.7  +0.0  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.58,  +57.99,  +24.00,   -2.72,  -63.63,  -26.70]  speed=20 wait=0.0s  (pre max|Δ|=4.65°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.32° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.4  +3.8  -4.6  +0.2  +1.3  -0.2  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.58,  +58.77,  +22.98,   -2.64,  -62.94,  -26.96]  speed=20 wait=0.0s  (pre max|Δ|=2.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.01° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.4  +0.1  -1.7  -0.3  +2.0  -0.3  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +13.58,  +58.77,  +22.98,   -2.64,  -62.94,  -26.96]  speed=20 wait=0.0s  (pre max|Δ|=2.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.01° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.4  +0.1  -1.7  -0.3  +2.0  -0.3  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 3.15°（関節 j4）
[005] 推論 872ms  10ステップ先  Δ= +1.3  -4.2  +0.2  -1.1  +2.7  +0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.58,  +55.83,  +25.98,   -3.16,  -62.25,  -26.58]  speed=20 wait=0.0s  (pre max|Δ|=3.84°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.64° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.5  -2.9  +1.5  -1.4  +3.8  +0.4  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +13.80,  +53.96,  +27.87,   -3.24,  -63.12,  -26.18]  speed=20 wait=0.0s  (pre max|Δ|=2.99°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.99° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.7  -3.0  +2.8  -0.9  +0.8  +0.7  着いた
[SEND] pi05_chunk005_via08: send_angles (single) [ +13.64,  +54.32,  +26.34,   -3.02,  -63.90,  -26.37]  speed=20 wait=0.0s  (pre max|Δ|=2.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.63° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  -2.6  +1.3  -0.6  -0.0  +0.5  着いた
[SEND] pi05_chunk005_via09: send_angles (single) [ +14.41,  +54.53,  +24.84,   -2.75,  -63.40,  -26.68]  speed=20 wait=0.0s  (pre max|Δ|=1.45°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.45° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.3  -1.8  -0.3  -0.4  +0.5  +0.0  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.45°（関節 j1）
[006] 推論 1824ms  10ステップ先  Δ= +5.8  -1.2  -8.0  +1.2  +2.1  -1.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +14.42,  +54.38,  +23.84,   -2.66,  -63.21,  -26.86]  speed=20 wait=0.0s  (pre max|Δ|=1.73°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.73° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  -1.6  -1.7  -0.3  +0.7  -0.2  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +14.96,  +54.46,  +22.18,   -2.38,  -63.15,  -27.11]  speed=20 wait=0.0s  (pre max|Δ|=2.78°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.51° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.9  -1.5  -3.0  -0.0  +0.5  -0.5  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +18.41,  +54.86,  +18.46,   -1.34,  -62.24,  -28.18]  speed=20 wait=0.0s  (pre max|Δ|=5.44°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.69° (tol 3.0°)
       [VIA   ] step  8  Δ= +4.2  -1.1  -5.8  +0.9  +0.5  -1.5  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +19.90,  +54.83,  +17.60,   -1.07,  -61.79,  -28.46]  speed=20 wait=0.0s  (pre max|Δ|=1.73°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.73° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.6  -1.2  -1.7  +0.8  -1.2  -1.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.73°（関節 j2）
[007] 推論 593ms  10ステップ先  Δ= +1.7  -1.2  -5.8  +1.1  -1.4  -1.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 7 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +55.98° → +46.93° まで（目標を 1 回送信。止めます）
[送信] j1 -1.0° → +19.86 +41.97 +19.24 -1.58 -60.38 -27.94  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 2.72°）  届いた角度 +19.86 +42.97 +19.24 -1.31 -63.10 -28.12
[長押し] j1 を +42.97° → +36.56° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (13.4 秒, 送った目標 4 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[008] 推論 566ms  10ステップ先  Δ=+16.2  +9.4  -9.1  +2.3  -0.2  -1.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > r
       爪を締め直します (開かずに閉じる指令をもう一度)
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk008_via02: send_angles (single) [ +28.32,  +31.66,  +17.26,   -0.14,  -59.02,  -28.87]  speed=20 wait=0.0s  (pre max|Δ|=8.46°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.76° (tol 3.0°)
       [VIA   ] step  2  Δ= +8.5  +0.5  -2.8  +1.2  +2.9  -0.7  着いた
[SEND] pi05_chunk008_via05: send_angles (single) [ +34.07,  +33.45,  +13.86,   +0.50,  -58.19,  -29.40]  speed=20 wait=0.0s  (pre max|Δ|=5.51°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.45° (tol 3.0°)
       [VIA   ] step  5  Δ= +5.6  +1.0  -5.3  +1.5  +3.5  -1.3  着いた
[SEND] pi05_chunk008_via08: send_angles (single) [ +36.03,  +38.69,  +11.44,   +0.85,  -60.05,  -29.56]  speed=20 wait=0.0s  (pre max|Δ|=4.33°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.56° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.6  +4.3  -3.9  +1.0  +0.6  -0.9  着いた
[SEND] pi05_chunk008_via09: send_angles (single) [ +36.02,  +40.59,  +10.97,   +0.96,  -61.01,  -29.56]  speed=20 wait=0.0s  (pre max|Δ|=2.03°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.68° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.0  +0.8  -2.0  +0.3  -0.4  -0.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.59°（関節 j2）
[009] 推論 551ms  10ステップ先  Δ= +3.7 +14.6  -5.6  +1.3  -7.8  -1.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk009_via02: send_angles (single) [ +37.46,  +44.02,   +8.30,   +1.39,  -61.43,  -29.89]  speed=20 wait=0.0s  (pre max|Δ|=4.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.45° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.4  +2.3  -4.3  +0.7  -0.6  -0.9  着いた
[SEND] pi05_chunk009_via05: send_angles (single) [ +38.69,  +49.85,   +7.83,   +1.61,  -65.11,  -29.99]  speed=20 wait=0.0s  (pre max|Δ|=4.77°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.48° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.2  +4.8  -1.9  +0.5  -4.1  -0.7  着いた
[SEND] pi05_chunk009_via08: send_angles (single) [ +39.76,  +55.07,   +7.24,   +1.84,  -68.18,  -30.16]  speed=20 wait=0.0s  (pre max|Δ|=5.86°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.96° (tol 3.0°)
       [VIA   ] step  8  Δ= +2.2  +6.6  -2.4  +0.6  -4.9  -0.7  着いた
[SEND] pi05_chunk009_via09: send_angles (single) [ +39.76,  +56.24,   +6.96,   +1.92,  -68.61,  -30.19]  speed=20 wait=0.0s  (pre max|Δ|=2.18°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.74° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +2.2  -2.4  +0.4  -1.8  -0.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.65°（関節 j2）
[010] 推論 781ms  7ステップ先  Δ= +0.1  +6.0  -2.9  +0.4  -6.2  -0.7  グリッパー→開
       [VIA   ] 中継点 3 点: step 2, 5, 6
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk010_via02: send_angles (single) [ +39.82,  +61.66,   +5.90,   +2.20,  -72.22,  -30.21]  speed=20 wait=0.0s  (pre max|Δ|=4.45°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.30° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.0  +4.5  -2.5  +0.6  -4.4  -0.4  着いた
[SEND] pi05_chunk010_via05: send_angles (single) [ +39.93,  +62.59,   +5.70,   +2.39,  -73.20,  -30.26]  speed=20 wait=0.0s  (pre max|Δ|=1.75°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.75° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.1  -0.2  -1.4  +0.7  -1.8  -0.5  着いた
[SEND] pi05_chunk010_via06: send_angles (single) [ +39.93,  +62.59,   +5.70,   +2.39,  -73.20,  -30.26]  speed=20 wait=0.0s  (pre max|Δ|=1.41°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.41° (tol 3.0°)
       [VIA   ] step  6  Δ= +0.1  -0.9  -1.4  +0.5  -1.0  -0.4  着いた
  [Pi] GRIPPER: OPEN result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.41°（関節 j2）
[011] 推論 1486ms  10ステップ先  Δ= -0.4 -17.9  -1.4  +0.6 +15.9  -1.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk011_via02: send_angles (single) [ +39.93,  +57.97,   +5.70,   +2.42,  -69.55,  -30.40]  speed=20 wait=0.0s  (pre max|Δ|=5.75°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.78° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.1  -5.8  -1.4  +0.2  +5.2  -0.4  着いた
[SEND] pi05_chunk011_via05: send_angles (single) [ +40.08,  +52.43,   +5.62,   +2.45,  -63.51,  -30.57]  speed=20 wait=0.0s  (pre max|Δ|=8.82°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.58° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.3  -7.6  -1.5  +0.2  +8.8  -0.5  着いた
[SEND] pi05_chunk011_via08: send_angles (single) [ +39.71,  +47.09,   +5.72,   +2.62,  -57.68,  -30.91]  speed=20 wait=0.0s  (pre max|Δ|=8.41°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.43° (tol 3.0°)
       [VIA   ] step  8  Δ= -0.1  -7.5  -1.4  +0.2  +8.4  -0.8  着いた
```

### 6回目 18:50 の端末の記録（失敗・1791〜1967行目）

#### 6回目（pi05_20261106_185050）

```text
[SEND] pi05_chunk011_via09: send_angles (single) [ +39.44,  +45.86,   +5.74,   +2.64,  -56.44,  -30.97]  speed=20 wait=0.0s  (pre max|Δ|=3.58°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.09° (tol 3.0°)
       [VIA   ] step  9  Δ= -0.4  -3.3  -1.4  +0.3  +3.7  -0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.09°（関節 j4）
[012] 推論 549ms  10ステップ先  Δ=-18.1 -23.5  -2.3  -2.5 +26.8  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=57.74°, post-send timeout 10.7s)
[SEND] arrival OK: max|Δ|=1.75° (tol 2.5°)
[RECORD] 記録終了 コマ 709 (8.186 fps) 実測角 753 回 (8.69 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184906/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > y
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_184906
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (92 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (6 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [0.00, 1.49, 1.75, 0.52, -0.43, -30.58]
[SEND] HOME へ移動指令
[WAIT] t=0.7s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [0.00, 1.49, 1.75, 0.43, -0.43, -30.58]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.2  dx=  +0.1  response=0.927  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (12 秒)

=== 8. 実機推論 (6 回目)

[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185050
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.4 dx=+7.3 response=0.544 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185050/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 589ms  10ステップ先  Δ= +3.3 +17.1 +11.3  -2.1 -22.4  +2.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.83,   +6.02,   +4.86,   +0.05,   -6.00,  -29.64]  speed=20 wait=0.0s  (pre max|Δ|=5.74°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.79° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  +4.5  +3.1  -0.2  -5.7  +0.6  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.88,  +11.66,   +8.58,   -0.58,  -13.47,  -28.77]  speed=20 wait=0.0s  (pre max|Δ|=9.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.79° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.1  +4.6  +2.7  -0.8  -9.3  +1.1  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +2.74,  +16.86,  +11.97,   -1.56,  -20.30,  -27.80]  speed=20 wait=0.0s  (pre max|Δ|=8.62°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.58° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.1  +4.2  +2.5  -1.1  -8.6  +1.2  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.34,  +18.63,  +13.09,   -1.87,  -22.68,  -27.45]  speed=20 wait=0.0s  (pre max|Δ|=3.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.15° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.8  +0.8  +0.3  -0.5  -4.0  +0.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.15°（関節 j4）
[001] 推論 808ms  10ステップ先  Δ= +3.4 +13.4  +8.3  -1.5 -22.4  +1.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +4.41,  +23.41,  +16.00,   -2.74,  -29.19,  -26.88]  speed=20 wait=0.0s  (pre max|Δ|=7.66°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.16° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.9  +3.9  +2.2  -1.1  -7.7  +0.8  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +5.77,  +27.90,  +18.72,   -3.20,  -36.04,  -26.63]  speed=20 wait=0.0s  (pre max|Δ|=8.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.15° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.5  +3.6  +1.9  -0.2  -8.0  +0.5  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +6.68,  +31.65,  +21.39,   -3.24,  -42.33,  -26.59]  speed=20 wait=0.0s  (pre max|Δ|=7.44°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.03° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.1  +2.8  +2.1  +0.4  -7.4  +0.3  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +6.91,  +32.92,  +22.09,   -3.21,  -43.93,  -26.56]  speed=20 wait=0.0s  (pre max|Δ|=2.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.91° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.1  +0.5  +0.2  +0.6  -2.6  +0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.91°（関節 j1）
[002] 推論 1594ms  10ステップ先  Δ= +3.0 +12.9  +7.5  +0.9 -19.6  +0.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +8.27,  +38.58,  +25.54,   -3.32,  -52.42,  -26.47]  speed=20 wait=0.0s  (pre max|Δ|=9.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.18° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.5  +4.7  +3.0  +0.4  -9.0  +0.3  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [  +9.52,  +43.71,  +28.88,   -3.17,  -59.99,  -26.44]  speed=20 wait=0.0s  (pre max|Δ|=8.75°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.85° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.5  +4.2  +3.0  +0.4  -8.8  +0.3  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [  +9.72,  +46.25,  +30.20,   -2.83,  -62.83,  -26.43]  speed=20 wait=0.0s  (pre max|Δ|=3.68°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.94° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.1  +1.6  +1.3  +0.9  -3.7  +0.2  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [  +9.75,  +46.70,  +30.04,   -2.83,  -63.01,  -26.44]  speed=20 wait=0.0s  (pre max|Δ|=0.50°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.84° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  -0.5  -0.2  +0.5  -0.3  +0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.84°（関節 j1）
[003] 推論 556ms  10ステップ先  Δ= +2.3  +8.8  -6.0  +0.5  -1.7  +0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53482]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be12c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b7988e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be13b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b7989d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be14d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be15c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be15e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be1778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53482]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b798d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x113be17c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +10.49 +47.54 +30.23 -3.33 -62.66 -26.63  届いた（ずれ 0.61°）  届いた角度 +10.10 +47.54 +30.23 -2.72 -62.66 -26.63
[送信] j0 +1.0° → +11.49 +47.54 +30.23 -3.33 -62.66 -26.63  届いた（ずれ 0.7°）  届いた角度 +11.51 +47.54 +30.23 -2.63 -62.66 -26.63
[送信] j0 +1.0° → +12.49 +47.54 +30.23 -3.33 -62.66 -26.63  届いた（ずれ 0.61°）  届いた角度 +12.74 +47.54 +30.23 -2.72 -62.66 -26.63
[PAD   ] 補正 1 を終えました (13.3 秒, 送った目標 3 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 843ms  10ステップ先  Δ= +0.8  +8.1  -4.3  -0.2  -1.2  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +12.85,  +48.23,  +30.48,   -2.44,  -63.18,  -26.67]  speed=20 wait=0.0s  (pre max|Δ|=0.69°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.69° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.2  +0.7  +0.3  -0.1  -0.5  -0.0  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.30,  +50.09,  +29.87,   -2.54,  -63.37,  -26.70]  speed=20 wait=0.0s  (pre max|Δ|=1.32°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.79° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.7  +2.3  -0.4  -0.2  -0.7  -0.1  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.55,  +54.24,  +27.08,   -2.57,  -63.77,  -26.81]  speed=20 wait=0.0s  (pre max|Δ|=3.27°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.95° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.3  +3.3  -3.2  -0.2  -0.2  -0.2  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +13.49,  +55.62,  +25.91,   -2.59,  -63.86,  -26.88]  speed=20 wait=0.0s  (pre max|Δ|=2.12°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.16° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.1  +0.5  -2.1  -0.3  -0.1  -0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.09°（関節 j4）
[005] 推論 1555ms  6ステップ先  Δ= +0.4  +2.8  -4.4  -0.4  +1.3  -0.7  グリッパー→閉
       [VIA   ] 中継点 2 点: step 2, 5
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.65,  +59.18,  +22.73,   -2.63,  -63.87,  -27.12]  speed=20 wait=0.0s  (pre max|Δ|=4.16°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.43° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  +2.8  -4.2  -0.3  +1.1  -0.5  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +13.69,  +59.18,  +22.48,   -2.68,  -63.63,  -27.28]  speed=20 wait=0.0s  (pre max|Δ|=1.67°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.67° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.3  -0.7  -0.8  -0.5  +1.7  -0.5  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 2.90°（関節 j4）
[006] 推論 806ms  10ステップ先  Δ= +1.5  -5.2  +0.6  -1.0  +2.7  +0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +13.67,  +54.78,  +26.36,   -2.96,  -62.86,  -26.75]  speed=20 wait=0.0s  (pre max|Δ|=5.07°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.43° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.5  -5.1  +3.3  -0.9  +3.0  +0.8  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +13.82,  +52.11,  +28.71,   -2.99,  -63.76,  -26.28]  speed=20 wait=0.0s  (pre max|Δ|=3.61°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.42° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.6  -3.6  +3.4  +0.3  -2.3  +0.9  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +13.87,  +54.52,  +24.64,   -2.83,  -63.84,  -26.81]  speed=20 wait=0.0s  (pre max|Δ|=3.30°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.23° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.7  +1.1  -3.3  +0.9  -2.5  -0.3  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +14.56,  +54.64,  +23.84,   -2.69,  -63.78,  -26.99]  speed=20 wait=0.0s  (pre max|Δ|=2.17°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.17° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.3  -0.5  -1.3  +0.9  -2.2  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.17°（関節 j4）
[007] 推論 1531ms  10ステップ先  Δ= +7.5  -2.8  -4.4  +1.7  -0.5  -2.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 7 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
```

### 7回目 18:52 の端末の記録（失敗・1968〜2157行目）

#### 7回目（pi05_20261106_185225）

```text
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +55.45° → +29.35° まで（目標を 3 回送信。止めます）
[PAD   ] 補正 2 を終えました (16.0 秒, 送った目標 3 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[008] 推論 825ms  10ステップ先  Δ=+11.6 +10.3  -2.7  +2.8  -4.1  -1.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=63.89°, post-send timeout 11.6s)
[SEND] arrival OK: max|Δ|=2.46° (tol 2.5°)
[RECORD] 記録終了 コマ 572 (7.99 fps) 実測角 627 回 (8.76 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185050/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > y n
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185050
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (79 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (7 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.26, 1.58, 1.75, -0.26, -0.43, -29.70]
[SEND] HOME へ移動指令
[WAIT] t=0.7s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [-0.26, 1.58, 1.75, -0.26, -0.43, -29.70]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.3  dx=  -0.5  response=0.923  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (13 秒)

=== 8. 実機推論 (7 回目)

[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185225
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-3.9 dx=+6.2 response=0.546 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185225/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 551ms  10ステップ先  Δ= +4.1 +18.3 +12.7  -1.8 -24.3  +2.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.84,   +6.95,   +5.66,   -0.22,   -7.41,  -29.37]  speed=20 wait=0.0s  (pre max|Δ|=7.15°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.14° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.1  +5.4  +3.9  +0.0  -7.1  +0.3  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.92,  +12.99,   +9.74,   -0.78,  -15.41,  -28.68]  speed=20 wait=0.0s  (pre max|Δ|=9.97°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.97° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.1  +5.1  +3.1  -0.6 -10.0  +1.0  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +3.26,  +18.00,  +13.24,   -1.75,  -22.34,  -27.81]  speed=20 wait=0.0s  (pre max|Δ|=8.90°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.60° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.6  +4.0  +2.6  -1.5  -8.9  +1.9  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.81,  +19.84,  +14.40,   -2.07,  -24.59,  -27.51]  speed=20 wait=0.0s  (pre max|Δ|=3.85°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.28° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.9  +0.9  +0.3  -1.6  -3.9  +1.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.22°（関節 j4）
[001] 推論 785ms  10ステップ先  Δ= +4.0 +14.8  +9.0  -1.6 -25.1  +1.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +5.15,  +24.55,  +17.40,   -2.65,  -31.62,  -27.03]  speed=20 wait=0.0s  (pre max|Δ|=8.16°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.04° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.4  +3.8  +2.3  -1.3  -8.2  +1.3  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +6.57,  +29.17,  +20.41,   -2.88,  -38.97,  -26.84]  speed=20 wait=0.0s  (pre max|Δ|=8.39°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.27° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.4  +3.6  +2.3  -0.7  -8.4  +0.8  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.76,  +33.82,  +23.28,   -2.85,  -46.12,  -26.73]  speed=20 wait=0.0s  (pre max|Δ|=8.42°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.30° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.9  +3.8  +2.5  -0.2  -8.4  +0.8  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +8.13,  +35.49,  +24.15,   -2.87,  -48.45,  -26.69]  speed=20 wait=0.0s  (pre max|Δ|=3.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.89° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  +0.7  +0.4  -0.3  -3.6  +0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.89°（関節 j1）
[002] 推論 1538ms  10ステップ先  Δ= +2.5  +9.7  +4.7  -0.6 -14.7  +0.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +9.17,  +40.37,  +26.86,   -2.73,  -55.44,  -26.68]  speed=20 wait=0.0s  (pre max|Δ|=7.72°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.86° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.5  +4.0  +2.3  -0.5  -7.7  +0.4  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [  +9.94,  +44.60,  +29.11,   -2.73,  -60.63,  -26.64]  speed=20 wait=0.0s  (pre max|Δ|=6.05°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.92° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.9  +3.4  +1.9  -0.5  -6.1  +0.4  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.12,  +46.17,  +29.49,   -2.71,  -62.48,  -26.62]  speed=20 wait=0.0s  (pre max|Δ|=2.37°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.37° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.0  +0.7  +0.2  -0.4  -2.4  +0.3  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.19,  +46.03,  +29.17,   -2.81,  -62.44,  -26.61]  speed=20 wait=0.0s  (pre max|Δ|=0.81°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.07° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  -0.2  -0.1  -0.6  -1.3  +0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.07°（関節 j1）
[003] 推論 717ms  10ステップ先  Δ= +2.4  +4.6  -1.1  -1.0  -2.1  +0.4
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53507]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed052c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d5888e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed053b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d5889d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed054d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed055c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed055e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed05778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53507]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10d588d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x12ed057c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +11.01 +47.10 +29.26 -2.28 -61.96 -26.89  届いた（ずれ 0.38°）  届いた角度 +10.63 +47.10 +29.26 -2.37 -61.96 -26.80
[送信] j0 +1.0° → +12.01 +47.10 +29.26 -2.28 -61.96 -26.89  届いた（ずれ 0.44°）  届いた角度 +11.77 +47.10 +29.26 -2.72 -61.96 -26.80
[PAD   ] 補正 1 を終えました (8.9 秒, 送った目標 2 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 607ms  10ステップ先  Δ= +1.5 +14.2  -9.1  +0.0  -0.3  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +12.18,  +53.36,  +26.72,   -2.87,  -63.20,  -26.72]  speed=20 wait=0.0s  (pre max|Δ|=6.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.31° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.1  +6.3  -2.5  +0.0  -1.2  +0.1  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.07,  +58.17,  +23.15,   -2.84,  -63.04,  -26.77]  speed=20 wait=0.0s  (pre max|Δ|=4.71°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.91° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.0  +3.9  -4.8  +0.1  +0.9  +0.0  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.54,  +60.84,  +20.80,   -2.87,  -62.48,  -26.95]  speed=20 wait=0.0s  (pre max|Δ|=3.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.99° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.7  +2.0  -3.1  -0.2  +2.5  -0.1  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +13.56,  +61.26,  +20.16,   -2.89,  -62.26,  -27.04]  speed=20 wait=0.0s  (pre max|Δ|=3.21°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.95° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.4  -0.2  -1.1  -0.5  +3.2  +0.0  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.95°（関節 j4）
[005] 推論 787ms  6ステップ先  Δ= +0.3  -0.4  -1.1  -0.1  +1.6  +0.7  グリッパー→閉
       [VIA   ] 中継点 2 点: step 2, 5
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.46,  +61.37,  +20.16,   -2.55,  -63.16,  -26.86]  speed=20 wait=0.0s  (pre max|Δ|=1.17°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.17° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.3  -0.4  -0.5  -0.0  +1.2  +0.6  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +13.47,  +61.43,  +20.13,   -2.51,  -63.58,  -26.75]  speed=20 wait=0.0s  (pre max|Δ|=0.75°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.75° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.3  -0.4  -0.5  +0.0  +0.8  +0.7  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 0.75°（関節 j4）
[006] 推論 1500ms  10ステップ先  Δ= +0.6  -7.2  +5.8  -0.8  +1.4  +1.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +13.53,  +58.14,  +23.15,   -2.88,  -62.40,  -26.28]  speed=20 wait=0.0s  (pre max|Δ|=3.55°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.27° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  -3.5  +3.1  -0.3  +1.9  +1.1  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +13.60,  +55.19,  +25.69,   -3.15,  -62.18,  -26.04]  speed=20 wait=0.0s  (pre max|Δ|=3.96°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.06° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.4  -4.0  +3.8  -0.3  +0.7  +0.4  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +13.79,  +54.25,  +26.45,   -3.24,  -62.90,  -26.00]  speed=20 wait=0.0s  (pre max|Δ|=2.00°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.29° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.6  -2.0  +1.8  +0.0  -1.1  +0.0  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +13.74,  +54.54,  +25.92,   -3.23,  -62.94,  -26.09]  speed=20 wait=0.0s  (pre max|Δ|=1.33°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.33° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  -0.9  +0.4  +0.2  -1.3  -0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.33°（関節 j4）
[007] 推論 813ms  10ステップ先  Δ= +0.5  -0.5  -6.5  +0.3  -2.1  -0.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 7 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +55.45° → +48.25° まで（目標を 1 回送信。止めます）
[長押し] j1 を +40.51° → +47.02° まで（目標を 1 回送信。止めます）
[送信] j1 +1.0° → +13.18 +54.61 +25.48 -3.42 -61.78 -25.48  届いた（ずれ 0.97°）  届いた角度 +13.35 +55.01 +26.27 -3.51 -62.75 -25.66
[PAD   ] 補正 2 を終えました (12.1 秒, 送った目標 4 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[008] 推論 1524ms  10ステップ先  Δ= +0.1  +0.3  -3.3  +0.3  -0.5  -0.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > r
       爪を締め直します (開かずに閉じる指令をもう一度)
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 3 (推論 8 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
```

### 8回目 18:54 の端末の記録（失敗・2158〜2353行目）

#### 8回目（pi05_20261106_185415）

```text
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +55.28° → +46.58° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 3 を終えました (10.4 秒, 送った目標 2 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[009] 推論 787ms  10ステップ先  Δ=+19.5  +2.7 -19.6  +3.7  +6.5  -3.5
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=61.52°, post-send timeout 11.2s)
[SEND] arrival OK: max|Δ|=1.75° (tol 2.5°)
[RECORD] 記録終了 コマ 637 (7.748 fps) 実測角 699 回 (8.5 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185225/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > n
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185225
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (88 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (8 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [0.43, 1.49, 1.75, -0.35, -0.52, -29.61]
[SEND] HOME へ移動指令
  [WARN] daemon_get_angles failed: daemon error: get_angles returned empty/invalid after 12 retries
[WARN] get_angles 失敗 (試行 1/3)
  [WARN] daemon_get_angles failed: daemon error: get_angles returned empty/invalid after 12 retries
[WARN] get_angles 失敗 (試行 2/3)
  [WARN] daemon_get_angles failed: daemon error: get_angles returned empty/invalid after 12 retries
[WARN] get_angles 失敗 (試行 3/3)
[WAIT] t=7.5s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [0.08, 1.49, 1.75, -0.26, -0.26, -29.70]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.3  dx=  +0.4  response=0.922  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (20 秒)

=== 8. 実機推論 (8 回目)
[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185415
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.4 dx=+7.5 response=0.544 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185415/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 572ms  10ステップ先  Δ= +3.4 +16.1 +11.4  -1.7 -23.3  +1.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.99,   +5.80,   +4.80,   -0.40,   -6.25,  -29.59]  speed=20 wait=0.0s  (pre max|Δ|=5.99°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.21° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.9  +4.3  +3.0  -0.1  -6.0  +0.1  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.89,  +10.89,   +8.51,   -0.76,  -14.06,  -29.18]  speed=20 wait=0.0s  (pre max|Δ|=9.76°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.94° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.6  +4.3  +2.7  -0.5  -9.9  +0.5  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +3.13,  +15.89,  +12.06,   -1.64,  -21.35,  -28.40]  speed=20 wait=0.0s  (pre max|Δ|=9.23°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.02° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.9  +4.1  +2.6  -1.5  -9.2  +1.3  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.45,  +17.58,  +13.18,   -1.96,  -23.51,  -28.12]  speed=20 wait=0.0s  (pre max|Δ|=4.18°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.28° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.5  +0.9  +0.3  -1.5  -4.2  +1.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.28°（関節 j4）
[001] 推論 782ms  10ステップ先  Δ= +4.2 +12.8  +8.3  -1.9 -21.0  +2.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +4.78,  +22.14,  +16.20,   -2.58,  -29.49,  -27.49]  speed=20 wait=0.0s  (pre max|Δ|=7.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.02° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.3  +3.6  +2.3  -1.5  -7.3  +1.5  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +5.95,  +26.29,  +18.85,   -2.67,  -35.78,  -27.21]  speed=20 wait=0.0s  (pre max|Δ|=7.31°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.51° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.3  +3.3  +1.7  -0.5  -7.3  +1.0  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.37,  +30.35,  +21.51,   -2.73,  -41.75,  -27.11]  speed=20 wait=0.0s  (pre max|Δ|=7.48°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.41° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.4  +3.2  +2.0  -0.4  -7.5  +0.8  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +7.73,  +31.33,  +22.20,   -2.73,  -43.23,  -27.06]  speed=20 wait=0.0s  (pre max|Δ|=2.89°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.45° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  +0.0  +0.1  -0.3  -2.9  +0.7  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.05°（関節 j4）
[002] 推論 1463ms  10ステップ先  Δ= +4.0 +12.3  +6.8  -0.1 -19.3  +0.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +8.83,  +34.97,  +24.43,   -2.61,  -48.39,  -26.91]  speed=20 wait=0.0s  (pre max|Δ|=6.12°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.97° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.6  +2.7  +2.4  -0.3  -6.1  +0.5  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [ +10.27,  +39.43,  +26.81,   -2.49,  -54.95,  -26.83]  speed=20 wait=0.0s  (pre max|Δ|=7.41°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.73° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.5  +3.5  +1.9  -0.2  -7.4  +0.5  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +11.02,  +43.39,  +28.57,   -2.42,  -60.22,  -26.76]  speed=20 wait=0.0s  (pre max|Δ|=5.91°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.99° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.6  +3.2  +1.4  -0.2  -5.9  +0.5  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +11.20,  +44.45,  +28.86,   -2.35,  -61.46,  -26.75]  speed=20 wait=0.0s  (pre max|Δ|=1.79°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.90° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.1  +0.1  +0.2  -0.2  -1.8  +0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.90°（関節 j1）
[003] 推論 791ms  10ステップ先  Δ= +2.8  +4.0  +0.6  -0.7  -2.1  +0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53542]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca12c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a88e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca13b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a89d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8a20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8a98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca14d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8ae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8b38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8b88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca15c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8bb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca15e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8bd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8c50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8cc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8d18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8d40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca1778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53542]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x10b6a8d90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x10fca17c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +12.07 +45.35 +28.65 -2.02 -60.90 -27.07  届いた（ずれ 1.59°）  届いた角度 +12.21 +45.35 +28.65 -1.84 -62.49 -26.98
[送信] j0 +1.0° → +13.07 +45.35 +28.65 -2.02 -60.90 -27.07  届いた（ずれ 1.59°）  届いた角度 +12.83 +45.35 +28.65 -1.84 -62.49 -26.98
[PAD   ] 補正 1 を終えました (9.8 秒, 送った目標 2 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 1547ms  10ステップ先  Δ= +0.5 +16.7 -10.0  +0.1  -3.4  -0.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +12.86,  +51.28,  +28.20,   -1.89,  -67.02,  -26.88]  speed=20 wait=0.0s  (pre max|Δ|=5.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.46° (tol 3.0°)
       [VIA   ] step  2  Δ= -0.3  +5.9  -0.5  +0.0  -4.5  +0.1  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.28,  +56.33,  +24.31,   -1.83,  -67.14,  -26.86]  speed=20 wait=0.0s  (pre max|Δ|=4.34°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.58° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.1  +4.1  -4.3  +0.1  -1.6  +0.1  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.54,  +60.89,  +19.66,   -1.88,  -66.11,  -26.95]  speed=20 wait=0.0s  (pre max|Δ|=5.65°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.73° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.4  +3.8  -5.6  +0.0  -0.5  +0.0  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +13.73,  +62.01,  +18.64,   -1.87,  -65.86,  -27.01]  speed=20 wait=0.0s  (pre max|Δ|=1.75°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.95° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  +0.4  -1.8  -0.0  -0.2  -0.0  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.87°（関節 j2）
[005] 推論 641ms  3ステップ先  Δ= +0.3  -0.7  -1.0  -0.3  +1.0  +0.0  グリッパー→閉
       [VIA   ] 中継点 1 点: step 2
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.74,  +62.09,  +18.50,   -1.82,  -65.71,  -27.06]  speed=20 wait=0.0s  (pre max|Δ|=1.01°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.01° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.3  -0.7  -1.0  -0.3  +1.0  +0.0  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.70°（関節 j4）
[006] 推論 691ms  10ステップ先  Δ= +1.6  -7.5  +3.2  -1.9  +6.6  +1.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk006_via02: send_angles (single) [ +14.17,  +60.21,  +19.19,   -2.35,  -63.09,  -26.77]  speed=20 wait=0.0s  (pre max|Δ|=4.67°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.65° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.7  -2.6  -0.2  -1.0  +4.7  +0.6  着いた
[SEND] pi05_chunk006_via05: send_angles (single) [ +14.71,  +56.36,  +22.07,   -2.99,  -60.43,  -26.30]  speed=20 wait=0.0s  (pre max|Δ|=5.31°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.47° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.3  -5.3  +2.6  -1.4  +5.3  +0.9  着いた
[SEND] pi05_chunk006_via08: send_angles (single) [ +14.79,  +54.84,  +23.55,   -3.22,  -60.62,  -26.02]  speed=20 wait=0.0s  (pre max|Δ|=2.99°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.58° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.3  -3.0  +2.0  -0.5  +1.1  +0.5  着いた
[SEND] pi05_chunk006_via09: send_angles (single) [ +15.08,  +55.36,  +22.63,   -3.11,  -60.81,  -26.13]  speed=20 wait=0.0s  (pre max|Δ|=1.06°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.06° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  -1.1  -0.3  -0.2  +0.4  +0.1  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.06°（関節 j1）
[007] 推論 571ms  10ステップ先  Δ= +0.8  -1.6  -3.5  +0.1  -0.7  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk007_via02: send_angles (single) [ +15.16,  +54.92,  +22.20,   -2.97,  -61.37,  -26.21]  speed=20 wait=0.0s  (pre max|Δ|=1.50°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.50° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.7  -1.5  -0.7  -0.1  -0.1  -0.0  着いた
[SEND] pi05_chunk007_via05: send_angles (single) [ +15.15,  +54.77,  +21.03,   -2.87,  -61.61,  -26.33]  speed=20 wait=0.0s  (pre max|Δ|=1.90°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.65° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.6  -1.7  -1.9  +0.0  -0.4  -0.1  着いた
[SEND] pi05_chunk007_via08: send_angles (single) [ +14.99,  +54.84,  +20.03,   -2.82,  -61.91,  -26.37]  speed=20 wait=0.0s  (pre max|Δ|=1.94°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.94° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  -1.6  -1.9  +0.1  -1.3  -0.2  着いた
[SEND] pi05_chunk007_via09: send_angles (single) [ +15.27,  +54.83,  +19.40,   -2.78,  -61.91,  -26.42]  speed=20 wait=0.0s  (pre max|Δ|=2.04°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.78° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.8  -1.6  -2.2  +0.1  -1.3  -0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.69°（関節 j2）
[008] 推論 600ms  10ステップ先  Δ= +0.5  -1.9  -4.4  +0.2  -2.3  -0.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 8 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[止める] stop（released_while_sending）  止まった角度 +15.11 +54.05 +20.65 -2.98 -63.19 -26.01
```

### 9回目 18:55 の端末の記録（成功・2354行目〜最後）

#### 9回目（pi05_20261106_185554）と、かかった時間のまとめ

```text
[長押し] j1 を +54.05° → +50.71° まで（目標を 1 回送信。止めます）
[長押し] j1 を +46.31° → +46.31° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (18.0 秒, 送った目標 3 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[009] 推論 1464ms  10ステップ先  Δ=+21.1  +1.0 -13.2  +3.0  +4.8  -2.6
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=60.90°, post-send timeout 11.1s)
[SEND] arrival OK: max|Δ|=2.46° (tol 2.5°)
[RECORD] 記録終了 コマ 599 (8.097 fps) 実測角 647 回 (8.75 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185415/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > n
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185415
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (82 秒)
もう1回やりますか? y=やる / 他=終わる > y

=== 7. HOME → 爪を開く → 位置合わせ (9 回目)
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [0.43, 1.49, 1.75, -0.35, -0.43, -29.70]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=1.75°
[DONE] HOME 到達 max_err=1.75°
[POST] 最終姿勢: [0.35, 1.49, 1.75, -0.26, -0.43, -29.70]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
  [Pi] GRIPPER: OPEN result= None
[REF   ] 基準画像: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818/frames/000000.jpg
[REF   ] お手本 run_0001_20261019_194818 (つかんだ瞬間の j0 = +13.2°)
[REF   ] 重なった画像のブロックに実物のブロックを合わせると, お手本と同じ位置に置ける
[ALIGN ] |dy| <= 20 なら OK. アームは HOME に置いた状態で合わせる. q または Esc で終了 (ファイルには何も書かない)
[ALIGN ] dy=  +2.1  dx=  +0.4  response=0.921  → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[DONE  ] 終了 (何も書いていません)
    (15 秒)

=== 8. 実機推論 (9 回目)
[WAIT  ] 送ったあと 0.0s / 爪のあと 0.8s (開くときの daemon 0.0s) / 止まるのを待つ目安 0.8s
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185554
[MODEL ] corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
[CAMERA] 写り方 dy=-4.5 dx=+7.5 response=0.543 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185554/recording
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
[STATE ] 実測を渡す (直前に送った目標がない)
[000] 推論 566ms  10ステップ先  Δ= +3.5 +17.7 +11.5  -1.5 -24.0  +1.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[GRIP  ] 開始時にグリッパーを開く
  [Pi] GRIPPER: OPEN result= None
[SEND] pi05_chunk000_via02: send_angles (single) [  +0.80,   +7.32,   +5.40,   -0.43,   -7.87,  -29.49]  speed=20 wait=0.0s  (pre max|Δ|=7.61°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.34° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.7  +5.8  +3.7  -0.2  -7.6  +0.2  着いた
[SEND] pi05_chunk000_via05: send_angles (single) [  +1.40,  +12.70,   +8.84,   -0.72,  -15.15,  -29.04]  speed=20 wait=0.0s  (pre max|Δ|=9.53°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.80° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  +4.4  +2.4  -0.5  -9.5  +0.7  着いた
[SEND] pi05_chunk000_via08: send_angles (single) [  +2.86,  +17.47,  +12.12,   -1.51,  -22.05,  -28.17]  speed=20 wait=0.0s  (pre max|Δ|=8.70°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.75° (tol 3.0°)
       [VIA   ] step  8  Δ= +2.1  +3.8  +2.4  -1.3  -8.7  +1.5  着いた
[SEND] pi05_chunk000_via09: send_angles (single) [  +3.56,  +19.14,  +13.28,   -1.79,  -24.24,  -27.84]  speed=20 wait=0.0s  (pre max|Δ|=3.94°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.27° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  +0.8  +0.4  -1.4  -3.9  +1.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.27°（関節 j3）
[001] 推論 615ms  10ステップ先  Δ= +5.1 +14.1  +9.1  -2.7 -23.3  +2.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk001_via02: send_angles (single) [  +4.79,  +23.81,  +16.30,   -2.40,  -30.84,  -27.24]  speed=20 wait=0.0s  (pre max|Δ|=7.82°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.31° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.2  +3.8  +2.3  -1.5  -7.8  +1.6  着いた
[SEND] pi05_chunk001_via05: send_angles (single) [  +6.36,  +28.21,  +19.36,   -2.75,  -37.24,  -26.96]  speed=20 wait=0.0s  (pre max|Δ|=7.71°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.30° (tol 3.0°)
       [VIA   ] step  5  Δ= +1.9  +3.4  +2.2  -0.7  -7.7  +1.1  着いた
[SEND] pi05_chunk001_via08: send_angles (single) [  +7.69,  +32.84,  +22.36,   -3.13,  -44.49,  -26.81]  speed=20 wait=0.0s  (pre max|Δ|=8.55°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.43° (tol 3.0°)
       [VIA   ] step  8  Δ= +1.8  +3.8  +2.4  -0.8  -8.6  +0.9  着いた
[SEND] pi05_chunk001_via09: send_angles (single) [  +8.07,  +34.10,  +23.11,   -3.21,  -46.35,  -26.77]  speed=20 wait=0.0s  (pre max|Δ|=3.29°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.88° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.7  +0.3  +0.3  -0.8  -3.3  +0.6  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.88°（関節 j1）
[002] 推論 1662ms  10ステップ先  Δ= +2.2 +12.0  +7.1  -0.3 -19.4  +0.8
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk002_via02: send_angles (single) [  +9.10,  +38.42,  +25.61,   -3.05,  -53.16,  -26.73]  speed=20 wait=0.0s  (pre max|Δ|=7.55°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.96° (tol 3.0°)
       [VIA   ] step  2  Δ= +1.0  +3.4  +2.8  -0.4  -7.5  +0.5  着いた
[SEND] pi05_chunk002_via05: send_angles (single) [ +10.05,  +43.27,  +28.46,   -3.02,  -60.15,  -26.65]  speed=20 wait=0.0s  (pre max|Δ|=7.95°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.09° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.6  +4.0  +2.5  -0.2  -8.0  +0.5  着いた
[SEND] pi05_chunk002_via08: send_angles (single) [ +10.27,  +46.28,  +29.92,   -2.85,  -64.30,  -26.62]  speed=20 wait=0.0s  (pre max|Δ|=5.15°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.91° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.3  +2.1  +1.4  -0.3  -5.1  +0.5  着いた
[SEND] pi05_chunk002_via09: send_angles (single) [ +10.23,  +47.01,  +29.91,   -2.79,  -65.05,  -26.62]  speed=20 wait=0.0s  (pre max|Δ|=1.25°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.25° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.0  -0.2  -0.1  -0.3  -1.2  +0.3  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.25°（関節 j4）
[003] 推論 717ms  10ステップ先  Δ= +1.9 +10.2  -6.0  -0.5  -0.3  +0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
objc[53570]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffc890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1336912c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffc8e0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691318). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffc958) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691390). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffc980) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1336913b8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffc9d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691408). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffca20) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691458). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffca98) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1336914d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcae8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691520). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcb38) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691570). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcb88) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1336915c0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcbb0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1336915e8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcbd8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691610). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcc50) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691688). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffccc8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691700). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcd18) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691750). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcd40) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x133691778). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[53570]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x110ffcd90) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1336917c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
[PAD   ] 補正 1 (推論 3 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 +1.0° → +11.19 +47.19 +29.97 -2.54 -63.80 -26.89  届いた（ずれ 0.79°）  届いた角度 +11.25 +47.90 +29.97 -2.28 -64.59 -26.80
[送信] j0 +1.0° → +12.19 +47.19 +29.97 -2.54 -63.80 -26.89  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 0.79°）  届いた角度 +11.42 +47.90 +29.97 -2.37 -64.59 -26.80
[送信] j0 +1.0° → +13.19 +47.19 +29.97 -2.54 -63.80 -26.89  届いた（ずれ 0.79°）  届いた角度 +13.44 +47.90 +29.97 -2.46 -64.59 -26.80
[PAD   ] 補正 1 を終えました (9.9 秒, 送った目標 3 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[004] 推論 560ms  10ステップ先  Δ= +0.8 +12.2  -9.4  -0.2  -0.2  -0.6  グリッパー→閉
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk004_via02: send_angles (single) [ +13.45,  +52.38,  +27.55,   -2.91,  -65.49,  -26.62]  speed=20 wait=0.0s  (pre max|Δ|=4.48°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.19° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.3  +4.5  -2.4  -0.2  -0.9  +0.2  着いた
[SEND] pi05_chunk004_via05: send_angles (single) [ +13.72,  +57.70,  +22.89,   -2.96,  -65.33,  -26.78]  speed=20 wait=0.0s  (pre max|Δ|=5.85°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.20° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  +4.4  -5.9  -0.2  +1.1  +0.0  着いた
[SEND] pi05_chunk004_via08: send_angles (single) [ +13.97,  +60.10,  +20.52,   -2.93,  -64.79,  -27.38]  speed=20 wait=0.0s  (pre max|Δ|=3.29°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.74° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.8  +1.6  -3.3  -0.2  +1.7  -0.6  着いた
[SEND] pi05_chunk004_via09: send_angles (single) [ +13.97,  +60.10,  +20.52,   -2.93,  -64.79,  -27.38]  speed=20 wait=0.0s  (pre max|Δ|=1.74°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.74° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.1  -0.8  -0.9  -0.4  +1.7  -0.5  着いた
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[STATE ] 目標を渡す  目標と実測の最大差 2.35°（関節 j4）
[005] 推論 578ms  10ステップ先  Δ= +0.1  -3.2  -1.4  -1.0  +4.4  +0.7
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk005_via02: send_angles (single) [ +13.84,  +58.80,  +21.60,   -3.12,  -64.06,  -26.92]  speed=20 wait=0.0s  (pre max|Δ|=3.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.64° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.1  -2.1  +0.2  -0.6  +3.3  +0.5  着いた
[SEND] pi05_chunk005_via05: send_angles (single) [ +13.87,  +58.19,  +21.35,   -3.35,  -63.64,  -26.50]  speed=20 wait=0.0s  (pre max|Δ|=2.98°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.98° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.2  -2.3  -0.1  -0.8  +3.1  +0.8  着いた
[SEND] pi05_chunk005_via08: send_angles (single) [ +13.76,  +57.47,  +20.75,   -3.51,  -62.90,  -26.39]  speed=20 wait=0.0s  (pre max|Δ|=3.28°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.78° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.1  -2.6  -0.7  -0.9  +3.3  +0.6  着いた
[SEND] pi05_chunk005_via09: send_angles (single) [ +13.78,  +57.68,  +20.07,   -3.50,  -62.76,  -26.45]  speed=20 wait=0.0s  (pre max|Δ|=1.92°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.92° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.2  -1.5  -1.4  -0.6  +1.9  +0.4  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.92°（関節 j4）
[006] 推論 703ms  10ステップ先  Δ= +1.4  -2.1  -4.3  +0.1  -0.2  +0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > p
[PAD   ] 補正 2 (推論 6 のあと). ゲームパッドで直して, □ で推論に戻ります
コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[長押し] j1 を +59.15° → +53.43° まで（目標を 1 回送信。止めます）
[送信] j1 -1.0° → +13.62 +48.30 +21.44 -2.90 -64.51 -26.71  届いた（ずれ 4.22°）  届いた角度 +13.71 +47.98 +21.26 -3.95 -60.29 -25.83
[送信] j1 -1.0° → +13.62 +47.30 +21.44 -2.90 -64.51 -26.71  動かなかった（次の押しは この目標の先から）・無視されたので 2 回送り直した（ずれ 3.08°）  届いた角度 +13.71 +49.39 +21.26 -3.86 -61.43 -25.83
[送信] j1 -1.0° → +13.62 +46.30 +21.44 -2.90 -64.51 -26.71  手前で止まった（次の押しは この目標の先から）（ずれ 3.08°）  届いた角度 +13.71 +48.16 +21.26 -3.86 -61.43 -25.92
[止める] stop（released_while_sending）  止まった角度 +13.71 +46.66 +21.35 -3.86 -60.20 -26.01
[長押し] j1 を +46.84° → +39.81° まで（目標を 1 回送信。止めます）
[PAD   ] 補正 2 を終えました (15.0 秒, 送った目標 6 回). この姿勢から推論に戻ります
[STATE ] 実測を渡す (直前に送った目標がない)
[007] 推論 1527ms  10ステップ先  Δ= +7.4  +4.4  +5.3  +1.3  -8.0  -0.2
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk007_via02: send_angles (single) [ +15.88,  +35.60,  +23.66,   -3.14,  -60.84,  -26.13]  speed=20 wait=0.0s  (pre max|Δ|=2.17°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.65° (tol 3.0°)
       [VIA   ] step  2  Δ= +2.2  +0.5  +1.2  +0.6  -1.8  -0.1  着いた
[SEND] pi05_chunk007_via05: send_angles (single) [ +17.97,  +37.10,  +26.30,   -2.80,  -63.50,  -26.21]  speed=20 wait=0.0s  (pre max|Δ|=3.63°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.45° (tol 3.0°)
       [VIA   ] step  5  Δ= +2.5  +1.4  +3.8  +0.6  -1.7  -0.2  着いた
[SEND] pi05_chunk007_via08: send_angles (single) [ +20.55,  +38.78,  +27.93,   -2.52,  -65.80,  -26.23]  speed=20 wait=0.0s  (pre max|Δ|=3.14°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.94° (tol 3.0°)
       [VIA   ] step  8  Δ= +2.5  +0.7  +1.6  -0.4  -3.1  -0.2  着いた
[SEND] pi05_chunk007_via09: send_angles (single) [ +21.10,  +39.47,  +27.69,   -2.43,  -66.40,  -26.24]  speed=20 wait=0.0s  (pre max|Δ|=0.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=0.93° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.6  -0.3  -0.6  +0.0  -0.9  -0.2  着いた
[STATE ] 目標を渡す  目標と実測の最大差 0.93°（関節 j4）
[008] 推論 631ms  10ステップ先  Δ=+21.4  +4.8 -19.8  +4.2  +5.8  -4.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk008_via02: send_angles (single) [ +28.02,  +40.12,  +21.94,   -1.03,  -64.20,  -27.45]  speed=20 wait=0.0s  (pre max|Δ|=7.55°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.54° (tol 3.0°)
       [VIA   ] step  2  Δ= +7.6  +0.4  -6.4  +1.3  +1.3  -1.4  着いた
[SEND] pi05_chunk008_via05: send_angles (single) [ +34.74,  +41.25,  +15.34,   +0.35,  -61.71,  -28.82]  speed=20 wait=0.0s  (pre max|Δ|=7.77°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.09° (tol 3.0°)
       [VIA   ] step  5  Δ= +6.8  +0.2  -7.8  +1.6  +4.0  -2.2  着いた
[SEND] pi05_chunk008_via08: send_angles (single) [ +40.54,  +43.22,   +9.42,   +1.58,  -59.77,  -29.85]  speed=20 wait=0.0s  (pre max|Δ|=7.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.92° (tol 3.0°)
       [VIA   ] step  8  Δ= +5.7  +1.1  -7.1  +1.3  +4.0  -1.8  着いた
[SEND] pi05_chunk008_via09: send_angles (single) [ +41.87,  +44.52,   +8.46,   +1.86,  -59.72,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=2.17°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.82° (tol 3.0°)
       [VIA   ] step  9  Δ= +1.5  +0.4  -2.2  +0.2  +2.0  -0.8  着いた
[STATE ] 目標を渡す  目標と実測の最大差 1.73°（関節 j2）
[009] 推論 616ms  10ステップ先  Δ= +0.6 +15.2  -6.1  +1.6 -11.8  -0.9
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk009_via02: send_angles (single) [ +42.56,  +49.35,   +6.16,   +2.24,  -62.16,  -30.03]  speed=20 wait=0.0s  (pre max|Δ|=4.03°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.39° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.8  +3.7  -4.0  +0.5  -1.1  -0.6  着いた
[SEND] pi05_chunk009_via05: send_angles (single) [ +42.91,  +54.84,   +5.25,   +2.43,  -66.92,  -30.05]  speed=20 wait=0.0s  (pre max|Δ|=5.93°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.62° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.3  +4.5  -2.3  +0.8  -5.9  -0.6  着いた
[SEND] pi05_chunk009_via08: send_angles (single) [ +42.98,  +59.89,   +4.34,   +2.62,  -71.79,  -30.19]  speed=20 wait=0.0s  (pre max|Δ|=6.49°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.48° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  +3.8  -2.4  +0.7  -6.5  -0.7  着いた
[SEND] pi05_chunk009_via09: send_angles (single) [ +42.85,  +60.92,   +4.08,   +2.69,  -72.93,  -30.23]  speed=20 wait=0.0s  (pre max|Δ|=2.62°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.62° (tol 3.0°)
       [VIA   ] step  9  Δ= +0.3  -0.1  -1.6  +0.6  -2.6  -0.5  着いた
[STATE ] 目標を渡す  目標と実測の最大差 2.01°（関節 j4）
[010] 推論 1526ms  5ステップ先  Δ= +0.3  +1.0  -1.5  +0.8  -3.8  -0.5  グリッパー→開
       [VIA   ] 中継点 2 点: step 2, 4
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk010_via02: send_angles (single) [ +42.93,  +62.89,   +3.98,   +2.95,  -74.46,  -30.22]  speed=20 wait=0.0s  (pre max|Δ|=3.10°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.46° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.4  +0.8  -1.5  +0.7  -3.1  -0.4  着いた
[SEND] pi05_chunk010_via04: send_angles (single) [ +42.85,  +62.60,   +4.03,   +3.04,  -74.76,  -30.22]  speed=20 wait=0.0s  (pre max|Δ|=1.47°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.47° (tol 3.0°)
       [VIA   ] step  4  Δ= +0.3  -1.5  -1.4  +0.4  -0.2  -0.4  着いた
  [Pi] GRIPPER: OPEN result= None
[STATE ] 目標を渡す  目標と実測の最大差 1.47°（関節 j1）
[011] 推論 627ms  10ステップ先  Δ= +0.0 -18.3  -1.1  +0.3 +17.1  -1.1
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > 
[SEND] pi05_chunk011_via02: send_angles (single) [ +42.79,  +58.51,   +4.13,   +3.07,  -71.45,  -30.42]  speed=20 wait=0.0s  (pre max|Δ|=5.56°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.72° (tol 3.0°)
       [VIA   ] step  2  Δ= +0.3  -5.6  -1.3  +0.3  +3.1  -0.5  着いた
[SEND] pi05_chunk011_via05: send_angles (single) [ +42.91,  +52.83,   +4.09,   +3.01,  -64.91,  -30.62]  speed=20 wait=0.0s  (pre max|Δ|=9.26°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.59° (tol 3.0°)
       [VIA   ] step  5  Δ= +0.5  -7.6  -1.3  +0.2  +9.3  -0.6  着いた
[SEND] pi05_chunk011_via08: send_angles (single) [ +42.94,  +47.53,   +4.21,   +3.07,  -59.24,  -30.91]  speed=20 wait=0.0s  (pre max|Δ|=8.17°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.37° (tol 3.0°)
       [VIA   ] step  8  Δ= +0.5  -7.4  -1.1  +0.2  +8.2  -0.6  着いた
[SEND] pi05_chunk011_via09: send_angles (single) [ +42.57,  +45.73,   +4.31,   +3.06,  -57.41,  -30.97]  speed=20 wait=0.0s  (pre max|Δ|=4.20°, post-send timeout 8.0s)
h 
[SEND] [TIMEOUT] 8.0s 内に未到達: max|Δ|=3.92° actual=[ +42.45,  +49.65,   +5.36,   +2.98,  -59.85,  -30.58]
[SEND] リトライしません: pre→last max|Δ| 1.76° >= 1.0° (部分移動/ストール疑い)
       [VIA   ] step  9  Δ= +0.1  -3.9  -1.0  +0.1  +4.2  -0.4  未到達
       [WARN] 未到達. j4 の発熱などを確認してください
[STATE ] 目標を渡す  目標と実測の最大差 3.92°（関節 j1）
[012] 推論 552ms  10ステップ先  Δ=-17.2 -23.6  -1.9  -2.4 +26.5  -0.3
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > h
[SEND] pi05_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=0.0s  (pre max|Δ|=59.67°, post-send timeout 11.0s)
[SEND] arrival OK: max|Δ|=1.93° (tol 2.5°)
[RECORD] 記録終了 コマ 724 (8.249 fps) 実測角 747 回 (8.51 Hz)  → /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185554/recording
       結果: y=成功 / n=失敗 / Enter=記録しない > y
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261106_185554
[CLIENT] run_policy_client.py が終わった (終了コード 0. モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2))
    (93 秒)
もう1回やりますか? y=やる / 他=終わる > y 

=== かかった時間  モデル corr2 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)
  1. Pod を作る                      2 秒
  2. Pod が立ち上がるまで待つ     28 秒
  3a. スクリプトを送る         56 秒
  3b. huggingface_hub を入れ, 鍵を渡す     10 秒
  3c. モデルを取る (hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v2)     32 秒
  3d. openpi を取る (215abfb)       14 秒
  3e. uv sync                          21 秒
  3f. addon・設定・assets (pi05_mecharm_trim_corr_v4_lora)      2 秒
  4. サーバーの起動             44 秒
  5. Mac の 8000 番へのトンネル      2 秒
  6. デーモンの状態              3 秒
  7. HOME → 爪を開く → 位置合わせ (1 回目)     47 秒
  8. 実機推論 (1 回目)          199 秒
  7. HOME → 爪を開く → 位置合わせ (2 回目)     25 秒
  8. 実機推論 (2 回目)           79 秒
  7. HOME → 爪を開く → 位置合わせ (3 回目)     18 秒
  8. 実機推論 (3 回目)           98 秒
  7. HOME → 爪を開く → 位置合わせ (4 回目)     19 秒
  8. 実機推論 (4 回目)          119 秒
  7. HOME → 爪を開く → 位置合わせ (5 回目)     14 秒
  8. 実機推論 (5 回目)           92 秒
  7. HOME → 爪を開く → 位置合わせ (6 回目)     12 秒
  8. 実機推論 (6 回目)           79 秒
  7. HOME → 爪を開く → 位置合わせ (7 回目)     13 秒
  8. 実機推論 (7 回目)           88 秒
  7. HOME → 爪を開く → 位置合わせ (8 回目)     20 秒
  8. 実機推論 (8 回目)           82 秒
  7. HOME → 爪を開く → 位置合わせ (9 回目)     15 秒
  8. 実機推論 (9 回目)           93 秒

[LAB   ] Pod は残しています (id b6j7nc4m74h7ru). 片付けは ./lab_stop.sh --log Log22
[TUNNEL] 8000 番のトンネルを閉じました
```


## 9回の直しをならべると、直したのは毎回同じ2か所だった

9回の記録（KIOXIA の `runs/pi05_policy/pi05_20261106_*/log.jsonl`）から、直しの中身を数えました。

| 直し | 回数 | 動いた関節 | かかった時間 |
|---|---|---|---|
| つかむ前（推論3のあと） | 9回 | j0 9回（中央 +2.1°・+1.2〜+3.3°） | 中央 9.9秒・8.9〜20.1秒 |
| つかんだあと（推論6〜8のあと） | 10回 | j1 9回（中央 −25.2°・−43.8〜−12.0°）、j4 5回、j2 3回 | 中央 14.2秒・10.4〜33.4秒 |

[図解: LOG450_pad_correction_timeline.html](https://pixelstomotors.com/embeds/LOG450_pad_correction_timeline.html)

この2か所は、キーで直していた前の回（LOG422）と同じです。モデルを corr から corr2 に学び直しても、根元の向きが約2°足りないことと、つかんだあとの持ち上げが足りないことは変わっていません。corr2 の学習データ（corr_v4b）には、この2か所を直した記録が7本入っていました。

つかんだあとの直しのうち3回は、j1 と一緒に手首 j4 も +3〜17° 動いています。十字キーで動かしたのは j1 だけなので、j4 は指令なしに動いたことになります。1回目の +17.2° がいちばん大きく、この回は成功しています。手首の点検（依頼97）では、この動きも見ています。

#### log450_table.py の出力

```markdown
## 11/6 の9回 (pi05_20261106_*)

| 回 | 記録 | 結果 | 実行した推論 | 爪を閉じた推論 | 爪を開いた推論 | ゲームパッドの補正 (推論 何番のあと: 秒・送った回数・実測の変化) |
|---|---|---|---|---|---|---|
| 1 | pi05_20261106_183926 (18:39) | 成功 | 14 | [5] | [12] | 推論3: 17.9秒・2回・j0 +2.1° / 推論8: 33.4秒・7回・j1 -32.5°, j4 +17.2° |
| 2 | pi05_20261106_184312 (18:43) | 失敗 | 8 | [5] | [] | 推論3: 13.2秒・2回・j0 +2.0° / 推論8: 12.7秒・3回・j1 -26.7°, j2 +1.1°, j4 +3.6° |
| 3 | pi05_20261106_184452 (18:44) | 成功 | 10 | [4] | [9] | 推論3: 20.1秒・11回・j0 +3.3° / 推論6: 11.1秒・3回・j1 -25.2°, j2 +1.2°, j4 +3.8° |
| 4 | pi05_20261106_184650 (18:46) | 成功 | 12 | [5] | [11] | 推論3: 9.8秒・1回・j0 +1.2° / 推論7: 26.6秒・6回・j1 -43.8°, j2 +17.9° |
| 5 | pi05_20261106_184906 (18:49) | 成功 | 10 | [4] | [10] | 推論3: 9.4秒・2回・j0 +2.1°, j4 -1.4° / 推論7: 13.4秒・4回・j1 -24.8° |
| 6 | pi05_20261106_185050 (18:50) | 失敗 | 6 | [5] | [] | 推論3: 13.3秒・3回・j0 +3.2° / 推論7: 15.9秒・3回・j1 -29.2° |
| 7 | pi05_20261106_185225 (18:52) | 失敗 | 6 | [5] | [] | 推論3: 8.9秒・2回・j0 +2.0° / 推論7: 12.1秒・4回・変化 1° 未満 / 推論8: 10.4秒・2回・j1 -12.8°, j4 +2.7° |
| 8 | pi05_20261106_185415 (18:54) | 失敗 | 7 | [5] | [] | 推論3: 9.8秒・2回・j0 +2.1°, j4 -1.6° / 推論8: 18.0秒・3回・j1 -12.0° |
| 9 | pi05_20261106_185554 (18:55) | 成功 | 10 | [4] | [10] | 推論3: 9.9秒・3回・j0 +3.0° / 推論6: 15.0秒・6回・j1 -24.1°, j4 +6.2° |

## ゲームパッドの補正 (19回)

- つかむ前: 9回  かかった時間 中央 9.9秒・最短 8.9秒・最長 20.1秒・合計 112.1秒  送った回数の中央 2  受付なし 合計 0
- つかんだあと: 10回  かかった時間 中央 14.2秒・最短 10.4秒・最長 33.4秒・合計 168.5秒  送った回数の中央 3.5  受付なし 合計 0
- 合わせて: 19回  かかった時間 中央 13.16秒・最短 8.9秒・最長 33.4秒・合計 280.7秒  送った回数の中央 3  受付なし 合計 0
- つかむ前 に動いた関節: j0 9回 (中央 +2.1°・+1.2〜+3.3°), j4 2回 (中央 -1.5°・-1.6〜-1.4°)
- つかんだあと に動いた関節: j1 9回 (中央 -25.2°・-43.8〜-12.0°), j2 3回 (中央 +1.2°・+1.1〜+17.9°), j4 5回 (中央 +3.8°・+2.7〜+17.2°)
- 学習データの範囲の外で終えた補正: pi05_20261106_184650 推論7のあと j[2]

## 推論1回分を実行するのにかかった時間 (t_exec_end - t_exec_start)

| 日 | 記録の数 | 実行した推論 | 中央 | 最短 | 最長 | 中継点4点の推論の中央 |
|---|---|---|---|---|---|---|
| 10/31 | 10 | 65 | 6.65秒 | 5.58秒 | 11.07秒 | 6.64秒 (52回) |
| 11/01 | 12 | 68 | 6.63秒 | 2.39秒 | 30.21秒 | 6.62秒 (53回) |
| 11/06 | 9 | 83 | 1.72秒 | 0.55秒 | 9.74秒 | 1.71秒 (69回) |

## 1回の補正にかかった時間 (correction の accept 行の t_end - t_start)

| 日 | 直し方 | 補正の数 | 中央 | 最短 | 最長 | 合計 |
|---|---|---|---|---|---|---|
| 10/31 | キー (c) | 21 | 41.3秒 | 15.4秒 | 109.2秒 | 924.3秒 |
| 11/01 | キー (c) | 14 | 51.23秒 | 12.0秒 | 86.9秒 | 657.1秒 |
| 11/06 | ゲームパッド (p) | 19 | 13.16秒 | 8.9秒 | 33.4秒 | 280.6秒 |
```

#### log450_table.py（9回の直しと、推論1回分の実行時間をならべる）

```python
#!/usr/bin/env python3
"""log450_table.py — 11/6 (Log22) の実機推論9回を, ゲームパッドの補正と推論の実行時間でならべる (LOG450).

使い方: python3 log450_table.py /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy
  - 11/6 の9回: 回ごとの結果・つかんだ推論・離した推論・ゲームパッドの補正 (始めと終わりの実測の差)
  - 推論1回分を実行するのにかかった時間 (t_exec_end - t_exec_start) を 10/31・11/1・11/6 でならべる
元の記録は読むだけ (書かない).
"""
import glob
import json
import statistics as st
import sys
from datetime import datetime
from pathlib import Path

ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else "/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")


def load(d):
    rows = [json.loads(x) for x in open(d / "log.jsonl", encoding="utf-8")]
    try:
        trial = json.load(open(d / f"{d.name}.json", encoding="utf-8"))
    except FileNotFoundError:
        trial = {}
    return rows, trial


def med(xs):
    return round(st.median(xs), 2) if xs else None


print("## 11/6 の9回 (pi05_20261106_*)\n")
print("| 回 | 記録 | 結果 | 実行した推論 | 爪を閉じた推論 | 爪を開いた推論 | ゲームパッドの補正 (推論 何番のあと: 秒・送った回数・実測の変化) |")
print("|---|---|---|---|---|---|---|")
all_corr = []
for n, d in enumerate(sorted(ROOT.glob("pi05_20261106_*")), 1):
    rows, trial = load(d)
    ex = [r for r in rows if r.get("type") is None and r.get("executed")]
    close = [r["chunk"] for r in ex if r.get("grip_cmd") == 1]
    open_ = [r["chunk"] for r in ex if r.get("grip_cmd") == 0]
    cs = []
    for r in rows:
        if r.get("type") == "correction" and r.get("device") == "gamepad":
            a, b = r["start_pose"], r["measured_after"]
            dj = [(k, b[k] - a[k]) for k in range(5) if abs(b[k] - a[k]) >= 1.0]
            txt = ", ".join(f"j{k} {v:+.1f}°" for k, v in dj) or "変化 1° 未満"
            cs.append(f"推論{r['after_chunk']}: {r['duration_s']:.1f}秒・{r['steps_sent']}回・{txt}")
            all_corr.append({"run": d.name, "after": r["after_chunk"], "dur": r["duration_s"], "sent": r["steps_sent"],
                             "ignored": r.get("pad_ignored"), "dj": dj, "out": r.get("outside_envelope"),
                             "after_close": bool(close) and r["after_chunk"] >= close[0]})
    t0 = datetime.strptime(d.name[5:], "%Y%m%d_%H%M%S").strftime("%H:%M")
    res = {"success": "成功", "failure": "失敗"}.get(trial.get("outcome"), "記録なし")
    print(f"| {n} | {d.name} ({t0}) | {res} | {len(ex)} | {close} | {open_} | {' / '.join(cs)} |")

print("\n## ゲームパッドの補正 (19回)\n")
before = [c for c in all_corr if not c["after_close"]]
after = [c for c in all_corr if c["after_close"]]
for name, cs in (("つかむ前", before), ("つかんだあと", after), ("合わせて", all_corr)):
    ds = [c["dur"] for c in cs]
    print(f"- {name}: {len(cs)}回  かかった時間 中央 {med(ds)}秒・最短 {min(ds):.1f}秒・最長 {max(ds):.1f}秒・合計 {sum(ds):.1f}秒"
          f"  送った回数の中央 {med([c['sent'] for c in cs])}  受付なし 合計 {sum(c['ignored'] or 0 for c in cs)}")
for name, cs in (("つかむ前", before), ("つかんだあと", after)):
    js = {}
    for c in cs:
        for k, v in c["dj"]:
            js.setdefault(k, []).append(v)
    print(f"- {name} に動いた関節: " + ", ".join(f"j{k} {len(v)}回 (中央 {med(v):+.1f}°・{min(v):+.1f}〜{max(v):+.1f}°)"
                                                for k, v in sorted(js.items())))
print("- 学習データの範囲の外で終えた補正: " + (", ".join(f"{c['run']} 推論{c['after']}のあと j{c['out']}" for c in all_corr if c["out"]) or "なし"))

print("\n## 推論1回分を実行するのにかかった時間 (t_exec_end - t_exec_start)\n")
print("| 日 | 記録の数 | 実行した推論 | 中央 | 最短 | 最長 | 中継点4点の推論の中央 |")
print("|---|---|---|---|---|---|---|")
for day in ("20261031", "20261101", "20261106"):
    ds, d4 = [], []
    dirs = sorted(ROOT.glob(f"pi05_{day}_*"))
    for d in dirs:
        rows, _ = load(d)
        for r in rows:
            if r.get("type") is None and r.get("executed") and r.get("t_exec_start"):
                t = r["t_exec_end"] - r["t_exec_start"]
                ds.append(t)
                if len(r.get("sent_targets") or []) == 4 and r.get("grip_cmd") is None:
                    d4.append(t)
    if ds:
        print(f"| {day[4:6]}/{day[6:]} | {len(dirs)} | {len(ds)} | {med(ds)}秒 | {min(ds):.2f}秒 | {max(ds):.2f}秒 | {med(d4)}秒 ({len(d4)}回) |")

print("\n## 1回の補正にかかった時間 (correction の accept 行の t_end - t_start)\n")
print("| 日 | 直し方 | 補正の数 | 中央 | 最短 | 最長 | 合計 |")
print("|---|---|---|---|---|---|---|")
for day in ("20261031", "20261101", "20261106"):
    ds, dev = [], set()
    for d in sorted(ROOT.glob(f"pi05_{day}_*")):
        rows, _ = load(d)
        for r in rows:
            if r.get("type") == "correction" and r.get("action") == "accept":
                ds.append(r["t_end"] - r["t_start"])
                dev.add("ゲームパッド (p)" if r.get("device") == "gamepad" else "キー (c)")
    if ds:
        print(f"| {day[4:6]}/{day[6:]} | {' / '.join(sorted(dev))} | {len(ds)} | {med(ds)}秒 | {min(ds):.1f}秒 | {max(ds):.1f}秒 | {sum(ds):.1f}秒 |")
```


## 1回の直しは、キーの41〜51秒から、ゲームパッドで約13秒になった

| 日 | 直し方 | 直しの数 | 1回の直し（中央） | 最短〜最長 | 合計 |
|---|---|---|---|---|---|
| 10/31（LOG417） | キー（c） | 21回 | 41.3秒 | 15.4〜109.2秒 | 924.3秒 |
| 11/1（LOG422） | キー（c） | 14回 | 51.2秒 | 12.0〜86.9秒 | 657.1秒 |
| 11/6（この回） | ゲームパッド（p） | 19回 | 13.2秒 | 8.9〜33.4秒 | 280.6秒 |

数えたのは、直しを始めてから推論に戻るまでの時間です（log.jsonl の補正の行の t_end − t_start）。日によってモデルが違い（10/31 は最初のモデル、11/1 は corr、11/6 は corr2）、直した中身もまったく同じではないので、同じ条件で比べた数字ではありません。ただ、直した所は3日とも、つかむ前の根元とつかんだあとの持ち上げの2か所でした。

縮んだ分の多くは、つかんだあとの持ち上げです。キーの補正では、1回押すごとに6つの関節の指令を送り、着いたかを確かめてから次の押しを受け付けていたので、肩を20〜30°上げるのに十数回押して待つことになりました。ゲームパッドでは、十字キーを押し続ければ1回の送信で15°先まで動き、離したところで止まります。

- ねらい：AI の失敗を人が直す手間を減らし、直した記録を集めやすくする
- 変わったこと：1回の直しは中央 41〜51秒 → 13.2秒、9回分の直しの合計は280.6秒
- 見込める効果：同じ時間で集められる、人が直した成功の記録が増える。学習データを増やす作業の重さが、直す時間ではなく、ブロックを置き直して位置を合わせる時間（1回12〜25秒）と推論の時間（1回79〜199秒）の側に移った


## 推論1回分の実行は、6.6秒から1.7秒になった（LOG446 の daemon の直しが効いた）

推論1回分（中継点4点まで）を実行するのにかかった時間を、日ごとにならべました（log.jsonl の t_exec_end − t_exec_start）。

| 日 | 実行した推論 | 中央 | 最短〜最長 | 中継点4点の推論の中央 |
|---|---|---|---|---|
| 10/31 | 65回 | 6.65秒 | 5.58〜11.07秒 | 6.64秒（52回） |
| 11/1 | 68回 | 6.63秒 | 2.39〜30.21秒 | 6.62秒（53回） |
| 11/6 | 83回 | 1.72秒 | 0.55〜9.74秒 | 1.71秒（69回） |

前の回（LOG446）で、アームの中の daemon が関節の角度を読み直すときの待ちを0.2秒から0.02秒にし、送ったあとの記録用の読み取りを1回だけにしました。それまでは、指令を1つ送るたびに daemon の返事が約1.6秒（角度が読めないと最長2.6秒）遅れていて、中継点4点の推論1回分は約6.6秒かかっていました。推論でアームを動かす速さ（speed 20）は変えていないので、縮んだのは送るたびの待ちの分です。

- ねらい：ゲームパッドの受付なしを減らす（LOG446 で直した理由）
- 変わったこと：推論1回分の実行は中央 6.63秒 → 1.72秒。11/1 の実機で長く感じていた、動きの合間の待ちが短くなった
- 見込める効果：1回の推論で動かす時間が短くなる。ただし、この回は推論のたびに Enter を押して確かめているので、1回分の推論（79〜199秒）の多くは人の確かめの時間で、そこはまだ縮んでいない

もう1つ効いたのが、学習データの抜けです。daemon の返事が遅かったころは、送ってから返事が来るまで記録係が角度を読めず、動いている間の角度の約98%が記録から抜けていました（依頼57）。この回の記録では、動いた角度のうち抜けの中にあるのは2.2〜2.9%でした（依頼97の報告）。抜けた所を後から埋める処理（依頼46）に頼らずに済む記録になっています。


## 落ちた4回は、どれもつかんだあと、肩で持ち上げてからだった

失敗の4回は、どれもブロックをつかんだあとに落としています。

| 回 | 記録に残っている、終えるまでの流れ |
|---|---|
| 2回目（18:43） | 推論8のあとに肩を長押しで持ち上げ、推論に戻って推論9を1回実行したあと、h で終えた |
| 6回目（18:50） | 推論7のあとに肩の長押しで j1 を 55.45° から 29.35° まで持ち上げ、推論8は実行せずに h で終えた |
| 7回目（18:52） | 推論7のあとと推論8のあとの2回に分けて持ち上げ、そのあと h で終えた |
| 8回目（18:54） | 推論8のあとに肩の長押しで持ち上げ、推論9は実行せずに h で終えた |

録画を見ると、成功した回はつかんだあともブロックがまっすぐ爪に挟まっていました。落ちた6回目は、持ち上げたあと、ブロックが爪の中で斜めに回っています。

![上から撮った録画のコマ4枚。上の段は成功した1回目で、左が肩を長押しで持ち上げた直後（ブロックはまっすぐ挟まっている）、右が箱の上で爪を開いたところ。下の段は失敗した6回目で、左が爪を閉じた直後、右が肩を持ち上げた直後（ブロックが爪の中で斜めに回っている）](https://pixelstomotors.com/media/LOG450_03_lift_success_vs_failure.png)

[動画: 失敗した6回目（18:50）の録画。肩を長押しで持ち上げる直しの間に、ブロックが爪の中で斜めに回っていく。記録のコマを1秒10コマで並べたので、実際より少し速い](https://pixelstomotors.com/media/LOG450_02_failure_block_tilted.mp4)

考えられる原因は2つで、どちらかはまだ分かっていません。

- 長押しは速さ20（1秒に約40°）で動き出し、離したときは daemon の stop で止まる（行きすぎは約3°）。この加速と止まりの揺れで、浅くつかんだブロックが回った
- 爪を閉じる強さ（GRIP_VALUE 20）がもともと足りず、持ち上げるたびにブロックが滑っていた

成功した5回も同じ長押しで持ち上げているので、長押しだけが原因とは言えません。次に試すときは、持ち上げる前に r（爪の締め直し。開かずに閉じる指令をもう一度送る）を1回押す回と押さない回を比べると、どちらが効いているかの手がかりになります。

学習には成功した回だけを使うので、落ちた4回は学習データに入っていません。


## 18:58 推論の Pod を消した（23分21秒・約36円）

9回目のあとに、もう1回やるかの問いで終え、Mac で `./lab_stop.sh --log Log22` を打って Pod を消しました。

### lab_stop_20261106_185821.txt

#### lab_stop の端末の記録

```text
[STOP  ] Log22 の Pod: id b6j7nc4m74h7ru  GPU=NVIDIA A40  作った時刻 2026-11-06 18:35:07
[LOG   ] serve.log を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log22/serve.log (45 行)
[STOP  ] Terminate を送った (18:58:28)
[STOP  ] Pod (id b6j7nc4m74h7ru) が消えたことを API で確かめた
[COST  ] 借りていた時間 23 分 21 秒 (0.39 時間) × 1時間 0.59 ドル = 約 0.23 ドル (約 36 円. 1ドル=158円)
[TUNNEL] 8000 番のトンネルは残っていません
```

### 推論の Pod の記録 lab/Log22/pod/lab_pod.json

#### lab/Log22/pod/lab_pod.json

```json
{
  "id": "b6j7nc4m74h7ru",
  "gpu": "NVIDIA A40",
  "log": "Log22",
  "costPerHr": 0.59,
  "created_epoch": 1793957707,
  "created_at": "2026-11-06 18:35:07",
  "model": "corr2",
  "model_repo": "PixelstoMotors/pi05-mecharm-trim-corr-lora-v2",
  "terminated_epoch": 1793959108,
  "terminated_at": "2026-11-06 18:58:28",
  "rented_sec": 1401
}
```

### Pod のサーバーの記録 lab/Log22/serve.log

#### lab/Log22/serve.log

```text
INFO:root:Loading model...
INFO:2026-11-06 09:38:16,568:jax._src.xla_bridge:925: Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:jax._src.xla_bridge:Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:2026-11-06 09:38:16,572:jax._src.xla_bridge:925: Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:jax._src.xla_bridge:Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:absl:orbax-checkpoint version: 0.11.13
INFO:absl:Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x7a48d3860990>
INFO:absl:Restoring checkpoint from /workspace/ckpt/corr_v2_1499/params.
INFO:absl:[thread=MainThread] Failed to get flag value for EXPERIMENTAL_ORBAX_USE_DISTRIBUTED_PROCESS_ID.
INFO:absl:[process=0] /jax/checkpoint/read/bytes_per_sec: 1.2 GiB/s (total bytes: 6.3 GiB) (time elapsed: 5 seconds) (per-host)
INFO:absl:Finished restoring checkpoint in 5.24 seconds from /workspace/ckpt/corr_v2_1499/params.
INFO:openpi.shared.download:Downloading gs://big_vision/paligemma_tokenizer.model to /root/.cache/openpi/big_vision/paligemma_tokenizer.model
INFO:tqdm_loggable.tqdm_logging:Progress on: -/4264023 rate:- remaining:? elapsed:00:00 postfix:-
INFO:tqdm_loggable.tqdm_logging:Progress on: 4.07MiB/4.07MiB rate:4,258,924.6iB/s remaining:00:00 elapsed:00:01 postfix:-
INFO:root:Loaded norm stats from /workspace/openpi/assets/pi05_mecharm_trim_corr_v4_lora/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b
INFO:root:Loaded norm stats from /workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b
INFO:root:Creating server (host: cbf7cd00209d, ip: 172.19.0.2)
INFO:websockets.server:server listening on 0.0.0.0:8000
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 34704) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 34704) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 55720) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 55720) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 42322) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 42322) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 43368) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 43368) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 42586) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 42586) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 35838) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 35838) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 44282) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 44282) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 42840) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 42840) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 34420) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 34420) closed
```


## 依頼97：成功5本を点検し、次の学習データ corr_v5 を作って corr3 を学習する

成功した5回の記録で次のモデルを学習させるため、Claude Code への依頼を依頼箱（依頼のファイルを置くと、Mac の Claude Code が順に受け取って作業するしくみ）に置きました。頼んだのは3つです。

1. 成功5本の手首と関節を点検し、手首が指令なしに垂れた所のある回は学習に入れない（前の回、手首のサーボが弱った回を学習に入れないために作った点検 `pi05/diag_joint_hold.py` を使う）
2. 前の7本（corr_v4b）に、点検を通った回を足して corr_v5 を作る。ゲームパッドの直しは送り方が違う（1刻みは1関節だけ送り、長押しは15°先へ送って離したら止める）ので、学習データの作り方（`pi05/build_correction_episodes.py`）が正しく扱えるかを先に確かめる
3. corr2 から1,500ステップ続けて学習する設定（モデル名 corr3）を足し、Log22 として今夜の学習を始める

### 依頼文 20261106_97_ゲームパッドで直した成功5本でcorr_v5を作り続きの学習を回す.md

#### 依頼97の依頼文

```markdown
---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 120
---
# 依頼：ゲームパッドで直した今日の成功5本を点検し、補正データ corr_v5 を作り、corr2 から続きの学習を Log22 で回し始める（2026-11-06）

依頼57・58b（corr_v4 と corr2）の続き。今日（11/6）、モデル corr2（PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 の 1499）で実機推論を9回回した。
補正は、新しく足した p（ゲームパッドで補正。コミット cd2fb46。run_policy_client.py が gamepad_teleop.py を同じプロセスで動かす）だけを使った。キーの補正 c と巻き戻し b は使っていない。

## 今日の9回（KIOXIA の runs/pi05_policy/）

| run | 結果 | ゲームパッドの補正 |
|---|---|---|
| pi05_20261106_183926 | 成功 | 2 |
| pi05_20261106_184312 | 失敗 | 2 |
| pi05_20261106_184452 | 成功 | 2 |
| pi05_20261106_184650 | 成功 | 2（2回目は j2 が学習データの範囲の外で終えた） |
| pi05_20261106_184906 | 成功 | 2 |
| pi05_20261106_185050 | 失敗 | 2 |
| pi05_20261106_185225 | 失敗 | 3 |
| pi05_20261106_185415 | 失敗 | 2 |
| pi05_20261106_185554 | 成功 | 2 |

## A. 成功5本の手首と関節の点検（読むだけ）

- 依頼57の `pi05/diag_joint_hold.py` で、183926・184452・184650・184906・185554 を点検する。同じ分け方・同じ「4° を超えたら外れ」で、run ごとの表を出す
- ゲームパッドの補正は送り方が違う（1刻みは send_angle で動かす関節だけ・長押しは 15° 先へ1回送って離したら stop）。この区間でも点検が正しく読めるかを確かめ、読めない所があれば直す。補正の区間は log.jsonl の type=pad の行と type=correction・device=gamepad の行にある
- 外れのある回は入れない案にし、理由を書く

## B. 補正データ corr_v5 を作る

- 入れるのは、corr_v4 の7本（10-31 の 175527・180435・181946・184016、11-01 の 190347・190854・192101）と、A で入れてよいとした今日の回
- `pi05/build_correction_episodes.py` がゲームパッドの補正の記録を正しく扱うかを先に確かめる。特に次の3つ
  - 補正の間（状態 correct）のコマが学習に入ること。爪の指令の時刻（recorder.gripper）が states.csv に入っていること
  - 角度の抜けの埋め方（依頼46）：ゲームパッドの区間は送り方が違い、記録スレッドの読み取りも続いている。今日の回で抜けがどれだけあるかを測り、ゲームパッドの区間に作り直し（ウ）が要るか・合うかを書く
  - 止まりの詰め方（依頼48）：長押しを離して stop で止めた所や、1刻みのあとの止まりが、今までと同じ決まりで詰まるか
- 直す必要があれば直し、テストを足す。今までの corr_v1〜v4 の作り方は変えない（同じ入力から同じものができること）
- 依頼57と同じしきい値で作り、`mecharm_redblock_box_corr_v5` と、お手本と合わせた `..._teach_v1_trim2_corr_v5`、Pod に送る tar を作る。exFAT の `._` は入れない。corr_v1〜v4 は残す
- check_dataset を通し、run ごとの表（コマ・補正のコマ・止まりの割合・1コマの最大の動き）を出す

## C. 続きの学習の設定と、学習を回し始める

- 続きの元は corr2 の 1499（KIOXIA の `checkpoints/pi05_mecharm_trim_corr_lora_v2/1499`）。学習率の予定と正規化の値は依頼57と同じ考え方（正規化は trim2 の値のまま。1499 の値が trim2 と同じかを確かめてから置く）。ステップ数 1,500
- 設定名の案：`pi05_mecharm_trim_corr_v5_lora`。新しいモデルの Hugging Face のリポジトリ名の案：`pi05-mecharm-trim-corr-lora-v3`。KIOXIA の置き場 `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499`
- lab_train.sh と lab_start.sh の `--model` に `corr3` を足す。今までの trim・corr・corr2 は何も変えずに動くこと
- **Log 番号は Log22**（このチャットの番号）
- 今日の推論の Pod（Log22 の lab_pod.json、id b6j7nc4m74h7ru）がまだ残っていれば、先に `./lab_stop.sh --log Log22` で消してから学習を始める。記録（lab_pod.json）が学習の Pod とぶつからないことを確かめる
- テストと codex のレビューが通ったら、`./lab_train.sh --log Log22 --model corr3` を、依頼箱の作業が終わっても止まらない形（nohup などで裏に回す）で始める。学習が終わったらモデルを持ち帰って Pod を消す、今までの lab_train.sh の流れのまま
- Pod ができて、step が進み始めたことを確かめてからこの依頼を終える。学習が終わるのは待たない
- 始められなかったときは Pod を残さない（作ってしまったら消す）。理由を報告に書く

## 確かめ

- 既存のテストが全部通ること（pi05/test_rewind_correct.py の top.mp4 の1件は、cd2fb46 の前から Linux の環境で落ちるもの。Mac で通るかを書く）
- codex のレビューを通す（docs/ に依頼文と結果を残す）
- 測定データ（runs/・datasets/・checkpoints/ の既存のもの）は上書き・移動・削除しない。書く前に KIOXIA のマウントを確かめる
- アームは使わない
- git は個別ファイルを指定して add する

## 報告に書くこと

- A：run ごとの表と、入れる回・入れない回の案と理由
- B：ゲームパッドの区間の扱いで分かったこと・直したこと、corr_v5 の場所と run ごとの表
- C：学習を始めた時刻、Pod の id、GPU、出力のファイルの場所、終わる見込みの時刻
- 変えたファイル、コミット、テストの件数
- 決めてほしいこと
```

### 依頼97で分かったことと決めたこと

依頼97は18:59に始まり、20:08に終わりました（69分）。報告のうち、この回の結果にかかわるものは次のとおりです。

- 手首の点検の作り直し：daemon を直したあとは、送る命令が0.03〜0.14秒で返るので、アームは返事のあとも目標へ向かって動いています。前の点検をそのまま使うと、ふつうの動きを外れとして大量に数えていました。そこで、ゲームパッドの送信を log.jsonl の行と組にして指令を作り、送ってすぐ返った区間では、目標へ近づいた所から離れた分を見る形に直しました。11/1 の9本の結果は、前とまったく同じ値になっています
- 入れた回：183926・184452・184650・184906 の4本。185554（9回目）は、肩を長押しで持ち上げている間に、手首 j4 が指令 −64.5° のまま −63.5° から −57.7° まで動いた（止まったあとのずれ 6.8°）ので外した
- 学習データの作り方：直す必要はなかった。直している間（状態 correct）のコマは長押しで動いている途中も含めて入り、止まった所は前と同じ決まりで詰まることを、合成した記録のテスト（`pi05/test_pad_build.py`）で確かめた
- corr_v5：11本・1,584コマ（直しのコマ430・止まり14.9%）。前の7本は corr_v4b とバイト単位で同じ。お手本19本と合わせて30本・4,301コマ。Pod に送る tar は117MB
- 1コマ（0.1秒）の最大の動きが、前のデータ（3.3°まで）より大きくなった。推論で最大4.8°、ゲームパッドの長押しで j1 最大6.7°。daemon を直したあとの記録はこの形になる

報告の「決めてほしいこと」には、次のように答えました。

| 決めてほしいこと | 答え |
|---|---|
| 184650（4回目）を入れたままでよいか。直しの終わりに j2 が学習データの範囲（上限 +31.4°）を最大11.1°超えたコマが18あり、推論ではその端の約1°が切られる | 入れたまま。切られるのは約1°で、外すとデータを作り直して学習をやり直すことになる |
| 1コマの動きが前より大きいデータ（推論で最大4.8°、長押しで6.7°）を入れ続けてよいか | 入れる。daemon を直したあとの実機は推論でもこの速さで動くので、学習と推論の動きがそろう |
| 学習のテストの偽物のモデルの名前が前の版のまま。直すか | 次のアームの依頼のついでに直す（本番の流れには関係しない） |

### Claude Code の報告 20261106_97_..._20261106_1859.md

#### 依頼97の結果（Claude Code の報告）

```markdown
# 結果：20261106_97_ゲームパッドで直した成功5本でcorr_v5を作り続きの学習を回す

- 状態：完了
- 始めた時刻：2026-11-06 18:59
- 終わった時刻：2026-11-06 20:08（69分）
- 作業フォルダ：/Users/shimazueiichi/r2r2r-scripts
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume 2a5a4788-f085-45c0-9e7d-8aa031e8de2c`
- 止められた操作：1件（いちばん下に一覧）

## Claude Code の報告

## やったこと

点検・データ作り・学習の開始まで終えました。学習は Log22 の Pod で回っており、20:07 に step 44/1500 まで進んだのを確かめました。アームは使っていません。

**前提として分かったこと**
- 11-05 の daemon の直し（c7992ca）のあと、send_angles の rpc は 0.03〜0.14 秒で返ります（11-01 は約 1.6 秒）。腕は rpc が返ったあとも指令へ向かって動いています。
- そのため、依頼57の点検をそのまま使うと、ふつうの動きを「外れ」として大量に数えていました。

**A. 手首と関節の点検（pi05/diag_joint_hold.py を直した）**
- ゲームパッドの rpc 行（send_angle / stop）を log.jsonl の pad の行と組にして、指令を作るようにしました。
  - 1 刻みの指令は送った目標そのものです。
  - 長押しでは、押した関節は見ません（15° 先へ着く気がないため）。
  - stop のあとの指令は、止めた関節だけ止めたあとに読んだ角度にしました（gamepad_teleop と同じ決まり）。
  - 組にできなかった行は、今日の 5 本では 0 件です。
- rpc がすぐ返った区間では、指令へ向かう途中の動きを遅れに数えません。代わりに「いちばん指令へ近づいた所から離れた分」を見て、4° を超えたら外れにします。指令が変わらない間は区間をまたいで見ます。
- 11-01 の 9 本は、前の結果と全部同じ値になりました。

**B. corr_v5**
- build_correction_episodes.py は直す必要がありませんでした。合成の記録のテストを足して、次の 3 つを確かめました。
  - 補正の間（correct）のコマは、長押しで動いている途中も含めて入ります。
  - 補正の中の爪の指令は、その時刻で 0/1 が切り替わります。ただし今日の 9 回では、ゲームパッドで爪は使っていません。
  - 長押しを離して止まった所や 1 刻みのあとの止まりは、今までと同じ決まりで詰まります。
- 角度の抜けはほとんどなくなりました。動いた角度のうち抜けの中の割合は、11-01 の 98% に対して 2.2〜2.9% です。
- ゲームパッドの区間の抜けは、補正の始めの約 3.2 秒（窓を開く間で、腕はほぼ止まっている）と、長押しの途中の 0.3〜0.4 秒です。後者は speed20 で埋めても linear と同じ結果になるので、作り直し（ウ）は要りません。依頼57と同じ設定（speed20・詰め方 --trim-hold）で作りました。

**C. 続きの学習**
- `--model corr3` を lab_train.sh と lab_start.sh に足しました。trim・corr・corr2 の動きは変えていません。
- 続きの元 corr2 の 1499 の正規化の値は、trim2 と同じ（sha256 80346837…）であることを確かめてから置きました。
- 推論の Pod（b6j7nc4m74h7ru）は、18:58 にすでに消えていました。推論の記録は `lab/Log22/pod/`、学習の記録は `lab/Log22/train/` で、ぶつかりません。

## 結果

**A. run ごとの表（外れ = 4° 超）**

| run | j4 止まったあとのずれ 中央/最大 | j4 指令から離れた分の最大 | j1 中央/最大 | j2 中央/最大 | 外れ |
|---|---|---|---|---|---|
| 183926 | 0.8 / 2.2 | 1.2 | 0.9 / 2.1 | 0.9 / 2.2 | 0 |
| 184452 | 0.7 / 3.3 | 3.8 | 0.9 / 2.6 | 0.4 / 2.0 | 0 |
| 184650 | 0.8 / 3.0 | 2.7 | 1.1 / 2.6 | 1.0 / 1.9 | 0 |
| 184906 | 1.4 / 3.2 | 2.4 | 1.0 / 3.0 | 0.8 / 2.8 | 0 |
| 185554 | 1.5 / 6.8 | 5.8 | 0.9 / 2.1 | 0.7 / 1.7 | 5 区間（j4） |

- 入れる案: 183926・184452・184650・184906。184452 の離れた分 3.8° は、4° の目安の少し下です。
- 入れない案: 185554。補正 2 で j1 を長押ししている間に、j4 が指令 −64.5 のまま −63.5 から −57.7 まで動きました（止まったあとのずれ 6.8°）。推論に戻ったあとも −59 のままでした。

**B. corr_v5（/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_corr_v5）**
- 11 本 / 1584 コマ（補正のコマ 430・止まり 14.9%）です。前の 7 本は、corr_v4b とバイトで同じです。
- trim2 と合わせた `..._teach_v1_trim2_corr_v5` は 30 本 / 4301 コマ、Pod に送る tar は 117 MB で、`._` は 0 件です。
- check_dataset は FAIL なしです。WARN は止まりの割合（190854 22.8%・192101 22.1%）と、ブロックの位置が j0 +15° 付近に集まっていることで、どちらも依頼57と同じ内容です。
- 今日の 4 本の表:

| run | コマ | 補正のコマ | 止まり | 1 コマの最大の動き（j0〜j5, °） |
|---|---|---|---|---|
| 183926 | 188 | 34 | 8.0% | 3.81 5.94 3.07 1.15 4.89 0.61 |
| 184452 | 190 | 55 | 11.1% | 4.13 4.72 3.39 1.18 4.41 0.70 |
| 184650 | 178 | 38 | 7.3% | 4.44 6.68 4.76 1.03 4.08 0.73 |
| 184906 | 135 | 20 | 10.4% | 3.82 4.41 4.01 1.29 3.73 0.88 |

- 前の 7 本の数字は、依頼58の表と同じです。全部の表は logs/20261106_2010.md にあります。
- 1 コマの最大の動きが、今までより大きくなっています（trim2 は 3.3° まで）。推論で最大 4.8°、ゲームパッドの長押しで j1 最大 6.7° です。詰めたつなぎ目ではなく、実際の 0.1 秒の動きです。

**C. 学習**
- 始めた時刻: 19:51:43 に Pod を作り始め、19:56:56 に学習が始まりました。
- Pod: id uirn5j6iy4xbyx、NVIDIA A40、1 時間 0.59 ドルです。
- 進み: 20:07 に step 44/1500 まで進んだのを確かめました。
- 出力のファイル: `/Volumes/KIOXIA/r2r2r-data/lab/Log22/train/lab_train_20261106_195143.txt`（同じ場所に nohup_lab_train_20261106_195143.txt）。まとめは同じ場所の result_*.txt に出ます。
- モデルの置き場所: Hugging Face の `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3` と、KIOXIA の `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499` です。
- 終わる見込み: 22:45〜23:15 ごろ。学習が終われば、lab_train.sh がモデルを持ち帰って Pod を消します。上限の 11-07 00:51 を超えたときも消します。

**テスト（全部 OK）**
- pi05/ の Python テスト: diag_joint_hold 24、test_pad_build 3（新しく足したもの）、merge_and_config 27、rewind_correct 69 ほか。
- ほかのテスト: provenance 42（1 件は skip）、gamepad_teleop 90、servo_check_pi 15。
- シェルのテスト: test_lab_train.sh 214 件・test_lab_start.sh 153 件、どちらも NG 0。
  - test_lab_train.sh は、test_lab_start.sh と同時に流したときに時間を見る 1 件が NG になりました。1 本ずつ流すと全部 OK です。
- pi05/test_rewind_correct.py の top.mp4 の 1 件は、Mac では通りました。

**codex のレビュー（3 回）**
- 1 回目は必須 2 件でした（ゲームパッドの送信を次の送信と組にしうる／区間をまたいでゆっくり垂れるのを見落とす）。
- 2 回目は必須 1 件でした（角度の読みがない区間で、引き継ぎを切っていない）。
- 3 回目で必須がなくなりました。推奨のテスト 2 件も足しました。
- 直したあとも、A の結果（入れる 4 本・入れない 185554）は変わっていません。

## 変えたファイル
- pi05/diag_joint_hold.py
- pi05/test_diag_joint_hold.py
- pi05/test_pad_build.py（新しく足したもの）
- pi05/test_merge_and_config.py
- pi05/openpi_addon/add_trim_corr_config.py
- pi05/openpi_addon/pod_prep_trim_corr.sh
- lab_lib.sh
- lab_train.sh
- lab_start.sh
- lab_train_fakes.sh
- test_lab_train.sh
- test_lab_start.sh
- docs/diag_joint_hold_20261106.txt・.json
- docs/review_request_20261106_corr_v5_gamepad（_r2・_r3）.md
- docs/review_result_20261106_corr_v5_gamepad_codex（_r2・_r3）.md
- logs/20261106_2010.md

コミットは 284dfd7 と 6dac235 です。

## 決めてほしいこと
1. **184650 を入れたままでよいか。** 外れがないので入れましたが、補正 2 の終わりに j2 が trim2 の範囲（上限 +31.4°）を最大 11.1° 超えるコマが 18 あります。推論の安全範囲（trim2 +10°）では、この動きの端の約 1° が切られます。外すなら corr_v5b を作って学習し直しになります。
2. **1 コマの動きが大きいデータを入れ続けてよいか。** 推論で最大 4.8°、ゲームパッドの長押しで j1 最大 6.7° です（trim2 は 3.3° まで）。daemon の直しのあとの記録はこの形になります。
3. **据え置いた推奨の扱い。** 学習のテストの偽物のチェックポイントの assets 名が v3 のままです（テストだけの話で、本番の流れには影響しません）。次に直すかどうかを決めてください。

## 止められた操作

- {"tool_name": "Bash", "tool_use_id": "toolu_01Rw1Y1BCqsNsxnWonBdfA1b", "tool_input": {"command": "cd /Users/shimazueiichi/r2r2r-scripts; rm -rf /tmp/r97/root; mkdir -p /tmp/r97/root/datasets; R=\"--run pi05_20261031_175527 --run pi05_20261031_180435 --run pi05_20261031_181946 --run pi05_20261031_184016 --run pi05_20261101_190347 --run pi05_20261101_190854 --run pi05_20261101_192101 --run pi05_20261106_183926 --run pi05_20261106_184452 --run pi05_20261106_184650 --run pi05_20261106_184906\"; pyth
```

### 手首の点検の出力 docs/diag_joint_hold_20261106.txt

#### docs/diag_joint_hold_20261106.txt

```text
## pi05_20261106_183926  outcome=success corrections=2
   粗い最大   j0=  8.0 j1=  9.1 j2=  5.5 j3=  2.0 j4=  9.6 j5=  1.8
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.7 j1=  2.1 j2=  2.1 j3=  1.1 j4=  2.2 j5=  0.7
   止まり90%  j0=  0.5 j1=  1.9 j2=  1.6 j3=  0.6 j4=  1.8 j5=  0.6
   止まり中央 j0=  0.1 j1=  0.9 j2=  0.9 j3=  0.3 j4=  0.8 j5=  0.3
   止まり符号 j0= +0.0 j1= +0.9 j2= +0.9 j3= +0.1 j4= +0.2 j5= -0.1
   じわじわ   j0=  0.2 j1=  0.2 j2=  0.2 j3=  0.3 j4=  0.9 j5=  0.3
   向かう途中に離れた j0=  0.3 j1=  1.1 j2=  0.8 j3=  0.8 j4=  1.2 j5=  0.2  (すぐ返った区間 72・ゲームパッド {'pad_step': 9, 'pad_hold': 4, 'pad_stop': 4, 'pad_unknown': 0})
   段ごと {'correct': {'n': 17, 'j4_max': 1.67, 'j4_median': 0.18, 'j1_max': 2.0, 'j2_max': 1.3}, 'grip_after': {'n': 2, 'j4_max': 2.21, 'j4_median': 1.34, 'j1_max': 1.07, 'j2_max': 1.43}, 'model': {'n': 51, 'j4_max': 2.03, 'j4_median': 1.03, 'j1_max': 2.13, 'j2_max': 2.15}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.45, 0.54, 0.68, 0.53, -2.21, -0.25]
   爪 set_gripper (model) 前後の変化 [-0.09, 0.0, 0.0, 0.09, 0.0, -0.08]  爪のあとのずれ [0.0, 1.07, 1.43, -0.23, 0.47, -0.26]
   HOME 最後のずれ [-0.14, 1.48, 2.15, 0.3, -0.65, 0.08]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
   HOME 最後のずれ [0.0, 1.14, 1.4, -0.43, -0.43, -0.49]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, 0.0, None, 0.18]
   log chunk      n=16 j0=  2.1 j1= 31.5 j2=  2.1 j3=  0.9 j4= 16.6 j5=  0.7  (最大)
## pi05_20261106_184452  outcome=success corrections=2
   粗い最大   j0= 25.3 j1= 28.1 j2=  8.2 j3=  2.0 j4= 30.7 j5=  1.7
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  1.0 j1=  2.6 j2=  1.9 j3=  1.7 j4=  3.3 j5=  1.2
   止まり90%  j0=  0.8 j1=  1.3 j2=  1.2 j3=  1.1 j4=  2.0 j5=  0.6
   止まり中央 j0=  0.2 j1=  0.9 j2=  0.4 j3=  0.3 j4=  0.7 j5=  0.3
   止まり符号 j0= -0.1 j1= +0.9 j2= +0.4 j3= +0.1 j4= +0.6 j5= -0.1
   じわじわ   j0=  0.3 j1=  0.1 j2=  0.0 j3=  0.3 j4=  0.2 j5=  0.9
   向かう途中に離れた j0=  1.2 j1=  1.8 j2=  1.2 j3=  1.5 j4=  3.8 j5=  0.2  (すぐ返った区間 61・ゲームパッド {'pad_step': 11, 'pad_hold': 5, 'pad_stop': 4, 'pad_unknown': 0})
   段ごと {'correct': {'n': 20, 'j4_max': 2.99, 'j4_median': 0.62, 'j1_max': 1.84, 'j2_max': 1.1}, 'grip_after': {'n': 2, 'j4_max': 2.04, 'j4_median': 1.27, 'j1_max': 1.19, 'j2_max': 1.18}, 'model': {'n': 37, 'j4_max': 3.34, 'j4_median': 0.77, 'j1_max': 2.6, 'j2_max': 1.95}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.6, 0.84, 0.93, 0.41, -2.04, 0.13]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, -1.06, 0.0]  爪のあとのずれ [-0.51, 1.19, 1.18, -0.2, 0.49, 0.27]
   HOME 最後のずれ [0.23, 1.77, 1.02, 0.52, -0.84, -0.45]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.09]
   HOME 最後のずれ [-0.26, 1.4, 1.75, 0.7, -0.79, -1.02]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, -0.53, None, 0.0]
   log chunk      n=12 j0=  2.7 j1= 24.1 j2=  3.0 j3=  0.8 j4=  4.3 j5=  1.3  (最大)
## pi05_20261106_184650  outcome=success corrections=2
   粗い最大   j0= 22.0 j1= 24.3 j2=  7.2 j3=  1.9 j4= 30.5 j5=  1.9
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.8 j1=  2.6 j2=  1.9 j3=  0.9 j4=  3.0 j5=  0.6
   止まり90%  j0=  0.6 j1=  2.0 j2=  1.5 j3=  0.5 j4=  2.5 j5=  0.4
   止まり中央 j0=  0.1 j1=  1.1 j2=  1.0 j3=  0.3 j4=  0.8 j5=  0.2
   止まり符号 j0= -0.1 j1= +0.8 j2= +1.0 j3= -0.1 j4= +0.0 j5= +0.0
   じわじわ   j0=  0.3 j1=  0.1 j2=  0.5 j3=  0.2 j4=  0.3 j5=  0.2
   向かう途中に離れた j0=  0.4 j1=  2.6 j2=  1.3 j3=  1.1 j4=  2.7 j5=  0.2  (すぐ返った区間 59・ゲームパッド {'pad_step': 1, 'pad_hold': 6, 'pad_stop': 3, 'pad_unknown': 0})
   段ごと {'correct': {'n': 10, 'j4_max': 2.99, 'j4_median': 0.27, 'j1_max': 2.63, 'j2_max': 1.85}, 'grip_after': {'n': 3, 'j4_max': 2.84, 'j4_median': 2.11, 'j1_max': 1.49, 'j2_max': 1.29}, 'model': {'n': 43, 'j4_max': 2.8, 'j4_median': 0.75, 'j1_max': 2.16, 'j2_max': 1.82}, 'wait': {'n': 1, 'j4_max': 2.11, 'j4_median': 2.11, 'j1_max': 1.49, 'j2_max': 0.44}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.18, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.7, 0.62, 1.07, 0.38, -2.84, -0.31]
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [0.08, -1.49, 0.44, -0.26, -2.11, 0.0]
   爪 set_gripper (model) 前後の変化 [0.35, 0.09, -0.18, -0.27, 0.0, 0.0]  爪のあとのずれ [0.12, 1.21, 1.29, -0.06, 0.72, 0.33]
   HOME 最後のずれ [0.01, 1.79, 1.08, 0.66, -0.82, -0.57]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.18, 0.0]
   HOME 最後のずれ [-0.17, 1.49, 1.75, 0.52, -0.87, -0.93]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, None, None, None]
   log chunk      n=14 j0=  1.0 j1= 42.1 j2= 17.1 j3=  0.7 j4=  2.8 j5=  0.6  (最大)
## pi05_20261106_184906  outcome=success corrections=2
   粗い最大   j0= 39.7 j1= 47.5 j2=  7.1 j3=  2.5 j4= 55.9 j5=  1.7
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  1.0 j1=  3.0 j2=  2.8 j3=  0.9 j4=  3.1 j5=  1.4
   止まり90%  j0=  0.5 j1=  1.5 j2=  1.7 j3=  0.6 j4=  2.7 j5=  0.6
   止まり中央 j0=  0.1 j1=  1.0 j2=  0.8 j3=  0.3 j4=  1.4 j5=  0.2
   止まり符号 j0= -0.1 j1= +1.0 j2= +0.8 j3= +0.3 j4= -1.4 j5= +0.0
   じわじわ   j0=  0.3 j1=  0.2 j2=  0.3 j3=  0.4 j4=  1.4 j5=  1.0
   向かう途中に離れた j0=  0.3 j1=  1.0 j2=  0.8 j3=  0.9 j4=  2.4 j5=  0.0  (すぐ返った区間 53・ゲームパッド {'pad_step': 5, 'pad_hold': 3, 'pad_stop': 2, 'pad_unknown': 0})
   段ごと {'correct': {'n': 10, 'j4_max': 2.72, 'j4_median': 2.72, 'j1_max': 1.15, 'j2_max': 1.64}, 'grip_after': {'n': 3, 'j4_max': 3.15, 'j4_median': 1.59, 'j1_max': 1.13, 'j2_max': 1.54}, 'model': {'n': 37, 'j4_max': 2.67, 'j4_median': 0.87, 'j1_max': 2.99, 'j2_max': 2.83}, 'wait': {'n': 1, 'j4_max': 1.49, 'j4_median': 1.49, 'j1_max': 0.0, 'j2_max': 0.79}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.49, -0.06, 1.54, 0.8, -3.15, -0.02]
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [0.0, 0.0, 0.79, 0.27, -1.49, -0.18]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, -0.09]  爪のあとのずれ [-0.12, 1.13, 1.41, -0.2, -1.59, 0.29]
   HOME 最後のずれ [0.28, 2.04, 1.37, -0.18, -1.3, 0.3]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, -0.08, 0.0, 0.0, 0.35, 0.0]
   HOME 最後のずれ [0.0, 1.49, 1.75, 1.05, -1.05, -0.58]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, None, None, 0.09]
   log chunk      n=12 j0=  2.0 j1= 23.6 j2=  2.4 j3=  1.0 j4=  3.1 j5=  1.5  (最大)
## pi05_20261106_185554  outcome=success corrections=2
   粗い最大   j0= 42.5 j1= 49.6 j2=  7.7 j3=  3.2 j4= 59.7 j5=  2.0
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.8 j1=  2.1 j2=  1.7 j3=  1.2 j4=  6.8 j5=  1.2
   止まり90%  j0=  0.6 j1=  1.9 j2=  1.4 j3=  1.0 j4=  3.2 j5=  0.9
   止まり中央 j0=  0.1 j1=  0.9 j2=  0.7 j3=  0.4 j4=  1.5 j5=  0.5
   止まり符号 j0= -0.1 j1= +0.9 j2= +0.5 j3= +0.1 j4= +0.3 j5= +0.2
   じわじわ   j0=  0.2 j1=  0.1 j2=  0.6 j3=  0.3 j4=  1.1 j5=  0.2
   向かう途中に離れた j0=  0.5 j1=  1.1 j2=  1.0 j3=  1.0 j4=  5.8 j5=  1.0  (すぐ返った区間 57・ゲームパッド {'pad_step': 10, 'pad_hold': 3, 'pad_stop': 3, 'pad_unknown': 0})
   段ごと {'correct': {'n': 16, 'j4_max': 6.77, 'j4_median': 3.08, 'j1_max': 2.09, 'j2_max': 0.76}, 'grip_after': {'n': 2, 'j4_max': 2.53, 'j4_median': 1.38, 'j1_max': 1.47, 'j2_max': 1.41}, 'model': {'n': 37, 'j4_max': 5.45, 'j4_median': 1.36, 'j1_max': 1.92, 'j2_max': 1.73}}
   外れ (>4°) 5 区間 (5 区間・関節の組)
       38.2秒 correct pad_stop   j4 指令  -64.51 区間の頭  -59.67 → 終わり  -57.74  止まりのずれ 6.77  遅れ 0.0  離れた 5.8
       39.9秒 correct pad_step   j4 指令  -64.51 区間の頭  -59.32 → 終わり  -61.43  止まりのずれ 3.08  遅れ 0.0  離れた 4.22
       43.9秒 correct pad_stop   j4 指令  -64.51 区間の頭  -61.43 → 終わり  -59.50  止まりのずれ 5.01  遅れ 0.0  離れた 4.04
       45.3秒 correct pad_hold   j4 指令  -64.51 区間の頭  -59.50 → 終わり  -60.46  止まりのずれ 4.05  遅れ 0.0  離れた 4.04
       45.7秒 model   pad_stop   j4 指令  -64.51 区間の頭  -58.35 → 終わり  -59.06  止まりのずれ 5.45  遅れ 0.0  離れた 5.19
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.26, 0.8, 0.92, 0.39, -2.53, -0.04]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.32, 1.47, 1.41, -0.23, 0.23, 0.34]
   HOME 最後のずれ [-0.12, 3.92, 1.05, -0.08, -2.26, 0.39]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 1.67, 0.0]
   HOME 最後のずれ [0.08, 1.58, 1.93, 1.05, -0.26, -0.49]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, 0.0, None, None, 0.09]
   log chunk      n=12 j0=  3.0 j1= 22.6 j2=  2.3 j3=  1.3 j4=  4.4 j5=  1.1  (最大)
   区間ごと (j4): 秒 段 種類 指令 止まりのずれ 動き途中 止まり中の変化 止まった秒
        0.0 model   send         -7.87           0.00          0.0
        0.6 model   send        -15.15           0.00          0.0
        1.0 model   send        -22.05           0.00          0.0
        1.6 model   send        -24.25    1.23   0.00    0.0   1.3
        3.0 model   send        -30.84           0.00          0.0
        3.6 model   send        -37.24           0.00          0.0
        4.0 model   send        -44.49           0.00          0.0
        4.6 model   send        -46.34    0.73   0.00    0.0   2.2
        7.2 model   send        -53.16           0.00          0.0
        7.8 model   send        -60.15           0.00          0.0
        8.2 model   send        -64.30     0.5   0.00  -4.65   0.4
        8.7 correct send        -65.05    1.25   0.00    0.0   7.6
       16.4 correct pad_step    -63.80   -0.79   0.00    0.0   0.6
       17.4 correct pad_step    -63.80   -0.79   0.00    0.0   0.3
       17.8 correct pad_step    -63.80   -0.79   0.00    0.0   0.3
       18.2 correct pad_step    -63.80   -0.79   0.00    0.0   0.7
       19.0 model   pad_step    -63.80   -0.79   0.00    0.0   5.6
       24.6 model   send        -65.49   -0.95   0.00    0.0   0.1
       25.2 model   send        -65.33    -1.2   0.00  -0.09   0.4
       25.6 model   send        -64.79   -1.74   0.00    0.0   0.4
       26.1 model   send        -64.79   -1.74   0.00    0.0   0.0
       26.1 model   grip_after  -64.79   -2.53   0.00  -0.09   1.8
       29.0 model   send        -64.06   -2.56   0.00   0.08   0.1
       29.2 model   send        -63.64           0.00          0.0
       29.3 model   send        -62.90   -1.78   0.00    0.0   0.1
       29.6 correct send        -62.76   -0.78   0.00   1.14   8.2
       37.9 correct pad_hold    -64.51           0.00          0.0
       38.2 correct pad_stop    -64.51    6.77   0.00    0.0   1.2
       39.9 correct pad_step    -64.51    3.08   0.00    0.0   0.4
       40.9 correct pad_step    -64.51    3.08   0.00    0.0   0.3
       41.3 correct pad_step    -64.51    3.08   0.00    0.0   0.3
       41.6 correct pad_step    -64.51    3.08   0.00    0.0   0.6
       42.3 correct pad_step    -64.51    3.08   0.00    0.0   1.4
       43.7 correct pad_hold    -64.51    3.08   0.00    0.0   0.0
       43.9 correct pad_stop    -64.51    5.01   0.00    0.0   0.9
       45.3 correct pad_hold    -64.51    4.05   0.00    0.0   0.0
       45.7 model   pad_stop    -64.51    5.45   0.00    0.0   0.2
       52.2 model   send        -60.84           0.00          0.0
       52.4 model   send        -63.50           0.00          0.0
       52.7 model   send        -65.80    0.33   0.00    0.0   0.1
       53.3 model   send        -66.40    0.93   0.00    0.0   3.3
       56.8 model   send        -64.20   -1.54   0.00    0.0   0.4
       57.3 model   send        -61.71   -2.09   0.00    0.0   0.1
       57.9 model   send        -59.77   -1.92   0.00    0.0   0.2
       58.6 model   send        -59.72   -1.36   0.00   0.09   1.4
       60.2 model   send        -62.16    1.17   0.00    0.0   0.5
       60.8 model   send        -66.92    1.62   0.00  -4.31   0.4
       61.3 model   send        -71.79    1.48   0.00    0.0   0.0
       62.0 model   send        -72.93    1.57   0.00    0.0   2.1
       64.6 model   send        -74.46   -0.07   0.00    0.0   0.1
       65.0 model   send        -74.76    0.23   0.00    0.0   0.1
       65.0 model   grip_after  -74.76    0.23   0.00    0.0   2.4
       69.9 model   send        -71.45           0.00          0.0
       70.5 model   send        -64.91           0.00          0.0
       70.9 model   send        -59.24           0.00          0.0
       71.5 home    send        -57.41   -2.26   0.00   1.67   9.1
       81.2 home    send          0.00           0.00          0.0
[JSON] /tmp/r97/today4.json
```

### 依頼97の作業の記録 logs/20261106_2010.md

#### logs/20261106_2010.md

```markdown
# 2026-11-06 20:10 依頼 97 ゲームパッドで直した成功 5 本を点検し, corr_v5 を作り, corr2 から続きの学習 corr3 を Log22 で回し始める

アームは使っていない。KIOXIA のマウント（/Volumes/KIOXIA, exfat）を確かめてから、新しい名前のフォルダだけ作った（runs/・datasets/・checkpoints/ の既存のものは読むだけ）。

## 前提として分かったこと
- 11-05 の c7992ca のあと、send_angles の rpc は 0.03〜0.14 秒で返る（11-01 は 1.5〜1.6 秒）。腕は rpc が返ったあとも指令へ向かって動いている
- ゲームパッドの区間は states.csv に rpc 行（cmd=send_angle / cmd=stop）だけが残る。目標は log.jsonl の type=pad の行にある

## A. 手首と関節の点検（pi05/diag_joint_hold.py）
- 直したこと
  - rpc 行 send_angle は pad の move と組にする（±0.2 秒でいちばん近い・まだ組にしていないもの。送り直しは resends の数まで同じ行）。1 刻みの指令は command。長押し（no_check）では押した関節を見ない（15° 先へ着く気がない）。stop は halt と一対一で組み、指令は止めた関節だけ measured_after_halt（gamepad_teleop の _apply_halt と同じ）。組にできない行は pad_unknown（今日の 5 本は 0）
  - rpc が 0.5 秒より短く返った区間では、指令へ向かう途中の動きを遅れに数えない。代わりに関節ごとに「いちばん指令へ近づいた所から離れた分」（away）を見る。指令が変わらない間は区間をまたいで引き継ぐ。止まったあとのずれは、動き出したあとの止まりだけで見る
  - 11-01 の 9 本は、前の結果（docs/diag_joint_hold_20261101.json）と全部同じ
- 結果（外れ = 4° 超。docs/diag_joint_hold_20261106.txt・.json）

| run | j4 止まり 中央/最大 | j4 離れた分の最大 | j1 止まり 中央/最大 | j2 止まり 中央/最大 | 外れ |
| --- | --- | --- | --- | --- | --- |
| 183926 | 0.8 / 2.2 | 1.2 | 0.9 / 2.1 | 0.9 / 2.2 | 0 |
| 184452 | 0.7 / 3.3 | 3.8 | 0.9 / 2.6 | 0.4 / 2.0 | 0 |
| 184650 | 0.8 / 3.0 | 2.7 | 1.1 / 2.6 | 1.0 / 1.9 | 0 |
| 184906 | 1.4 / 3.2 | 2.4 | 1.0 / 3.0 | 0.8 / 2.8 | 0 |
| 185554 | 1.5 / 6.8 | 5.8 | 0.9 / 2.1 | 0.7 / 1.7 | 5 区間（j4） |

- 案: 入れる = 183926・184452・184650・184906。入れない = 185554（補正 2 で j1 を長押しで −15° 動かした間に、j4 が指令 −64.5 のまま −63.5 → −57.7。止めたあとのずれ 6.8°。推論に戻ったあとも −59 のまま）
- 184650 は外れがないが、補正 2 の終わりに j2 が trim2 の範囲（上限 +31.4°）を最大 11.1° 超える 18 コマがある（決めてほしいこと）

## B. corr_v5
- build_correction_episodes.py は変えていない。合成の記録で確かめた（pi05/test_pad_build.py）: correct のコマ（長押しで動いている途中も）が入る・補正の中の爪の指令（gripper_cmd 行）の時刻で 0/1 が切り替わる・長押しを離して止まった所は同じ決まりで詰まる・抜けは speed20 と linear で同じ
- 角度の抜け（0.25 秒超）: 入れる 4 本で、動いた角度のうち抜けの中の割合 model 2.2〜2.9%（11-01 は 98%）。correct の抜けは、補正の始めの約 3.2 秒（ゲームパッドの窓を開く間。腕はほぼ止まっていて 0〜1.4°）と、長押しの途中の 0.3〜0.4 秒（184650 の j1 −10.6°・j2 +13.0°）。後者は速度 20 の速さでは抜けいっぱいかかるので、speed20 で埋めても linear と同じ。→ ゲームパッドの区間には作り直し（ウ）は要らない（入れても変わらない）。corr_v4 と同じ speed20 のまま作った
- 止まり: 入れる 4 本の止まりは 7.3〜11.1%（今までの 7 本は 17.5〜22.8%）。correct の止まりのコマは 5〜12（今までの 7 本は 11〜23）
- 1 コマの最大の動きが大きい: model で最大 4.8°（腕が送ったらすぐ動くため）、ゲームパッドの長押しで j1 最大 6.7°（trim2 は 3.3° まで）。詰めたつなぎ目ではなく、実際の 0.1 秒の動き
- `datasets/mecharm_redblock_box_corr_v5`（`--gap-fill speed20 --trim-hold`）: 11 本 / 1584 コマ。前の 7 本は corr_v4b と episode.npz がバイトで同じ。お手本と一緒に使える: OK

| run | コマ | 補正のコマ | 止まり | 1 コマの最大 j0〜j5（°） |
| --- | --- | --- | --- | --- |
| pi05_20261031_175527 | 118 | 28 | 17.8% | 3.31 3.75 3.00 1.31 3.00 0.77 |
| pi05_20261031_180435 | 152 | 49 | 17.8% | 3.00 3.00 3.29 1.88 3.22 1.88 |
| pi05_20261031_181946 | 126 | 38 | 17.5% | 3.00 4.40 2.96 0.70 3.22 0.73 |
| pi05_20261031_184016 | 133 | 44 | 19.5% | 3.00 3.00 3.00 0.78 3.00 0.56 |
| pi05_20261101_190347 | 115 | 31 | 18.3% | 3.27 3.33 3.00 0.93 3.28 0.78 |
| pi05_20261101_190854 | 145 | 58 | 22.8% | 3.07 3.22 3.00 0.82 3.00 0.79 |
| pi05_20261101_192101 | 104 | 35 | 22.1% | 3.00 3.00 3.00 0.73 3.05 1.15 |
| pi05_20261106_183926 | 188 | 34 | 8.0% | 3.81 5.94 3.07 1.15 4.89 0.61 |
| pi05_20261106_184452 | 190 | 55 | 11.1% | 4.13 4.72 3.39 1.18 4.41 0.70 |
| pi05_20261106_184650 | 178 | 38 | 7.3% | 4.44 6.68 4.76 1.03 4.08 0.73 |
| pi05_20261106_184906 | 135 | 20 | 10.4% | 3.82 4.41 4.01 1.29 3.73 0.88 |
| 合計 | 1584 | 430 | 14.9% | |

- 合わせたもの `mecharm_redblock_box_teach_v1_trim2_corr_v5`: 30 本 / 4301 コマ・止まり 11.6%・tar 117 MB（._ 0 件・episode.npz 30）sha256 c5d43fa9…
- check_dataset: FAIL なし。corr_v5 単独 WARN 2（190854 22.8%・192101 22.1% の止まり／j0 +15° に 11/11）、合わせたもの WARN 1（同じ 2 本の止まり）
- corr_v1〜v4・v4b は残している

## C. 続きの学習 corr3（コミット 284dfd7）
- 続きの元 corr2 の 1499 の正規化（assets/local/..._trim2_corr_v4b/norm_stats.json）は sha256 80346837…で trim2（3999）と同じ
- add_trim_corr_config.py `--variant v5`（設定 pi05_mecharm_trim_corr_v5_lora・repo_id local/mecharm_redblock_box_teach_v1_trim2_corr_v5・学習率は v3 と同じ）、pod_prep_trim_corr.sh `v5`、lab_lib.sh の corr3（3c で PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → /workspace/ckpt/corr_v2_1499・init …/corr_v2_1499/params・exp trim_corr_v5・上げる先 PixelstoMotors/pi05-mecharm-trim-corr-lora-v3・KIOXIA checkpoints/pi05_mecharm_trim_corr_lora_v3/1499）、lab_start.sh `--model corr3`
- 推論の Pod（Log22 の b6j7nc4m74h7ru）は 18:58 に lab_stop.sh で消えていた（API で確かめた記録あり）。推論の記録は lab/Log22/pod/lab_pod.json、学習の記録は lab/Log22/train/lab_pod.json で別
- 19:51:43 に `./lab_train.sh --log Log22 --model corr3` を新しいセッションで裏に回した（pid 7138）
  - Pod id uirn5j6iy4xbyx・NVIDIA A40・1 時間 0.59 ドル。Pod の中で 30 本 / 4301 コマ・正規化 sha256 80346837…を確かめた
  - 19:56:56 学習開始。20:02 step 0、20:07 step 44/1500（進んでいる）
  - 上限 11-07 00:51（lab_train.sh が消す）
  - 出力: /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/lab_train_20261106_195143.txt（nohup 側の写し nohup_lab_train_20261106_195143.txt）。まとめは同じ所の result_*.txt
  - 終わる見込み: 22:45〜23:15 ごろ（Log17 の A40 で約 3 時間）

## テスト
- pi05/test_diag_joint_hold.py 24・test_pad_build.py 3（新）・test_merge_and_config 27・test_gap_fill 7・test_hf_upload_ckpt 10・test_pad_correction 4・test_recorder_rpc 4・test_rewind_correct 69（top.mp4 の 1 件も Mac では OK）・test_trim_hold 5・test_provenance 42（skip 1）・tools/test_gamepad_teleop 90・tools/test_servo_check_pi 15 すべて OK
- test_lab_train.sh OK 214 / NG 0・test_lab_start.sh OK 153 / NG 0（test_lab_start と同時に流したときは時間の上限を見る 1 件が NG。1 本ずつ流すと OK）

## レビュー
- codex R1（必須 2: pad の送信を次の送信と組にしうる・区間をまたぐ垂れを見落とす）→ R2（必須 1: 実測のない区間で引き継ぎを切らない）→ R3（必須なし・推奨 2 のテストを足した）。docs/review_*_20261106_corr_v5_gamepad*
- 据え置いた推奨: 184650 の範囲超え（推論の安全範囲 trim2 +10° で末端約 1° が切られる）・1 コマの動きが大きいことの評価・偽物のチェックポイントの assets が v3 のまま（テストだけ）
```

### codex のレビュー（3回）

Claude Code が書いたコードは、別の AI（codex）に読んでもらっています。1回目は直すべき点2つ（ゲームパッドの送信を次の送信と組にしてしまうことがある、区間をまたいでゆっくり垂れるのを見落とす）、2回目は1つ（角度の読みがない区間で引き継ぎを切っていない）、3回目で直すべき点がなくなりました。直したあとも、入れる4本と外す1本は変わっていません。

#### 1回目の依頼文 docs/review_request_20261106_corr_v5_gamepad.md

```markdown
# コードレビュー依頼：ゲームパッドで直した成功の点検・corr_v5・続きの学習 corr3（2026-11-06 依頼 97）

依頼 57・58（corr_v4 / corr2）の続き。11-06 にモデル corr2（PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 の 1499）で実機推論を 9 回回し、補正はゲームパッドの p（コミット cd2fb46。run_policy_client.py が tools/gamepad_teleop.py を同じプロセスで動かす）だけを使った。成功 5 本（183926・184452・184650・184906・185554）を点検し、外れのない回を corr_v5 に足して、corr2 の 1499 から続きの学習（Log22・--model corr3）を回す。

## 前提として分かったこと
- 11-05 の c7992ca（daemon の読み直しを 0.02 秒に）のあと、send_angles の rpc は 0.03〜0.14 秒で返る（11-01 は 1.5〜1.6 秒）。腕は rpc が返ったあとも指令へ向かって動いている
- ゲームパッドの区間は states.csv に rpc 行（cmd=send_angle / cmd=stop）しか残らず、目標は log.jsonl の type=pad の行（move の command・t_send・resends・no_check、halt の t_halt・halt_joint・measured_after_halt）にある
- 角度の抜け（0.25 秒超）は、今日の回ではほとんどない（入れる 4 本で、動いた角度のうち抜けの中の割合 model 2.2〜2.9%・correct 0.2〜32%。11-01 は 98%）。correct の抜けは、補正を始めたときの約 3.2 秒（ゲームパッドの窓を開く間。腕は止まっている）と、長押しの途中の 0.3〜0.4 秒（184650 の j1 −10.6°・j2 +13.0°）

## したこと
### A. pi05/diag_joint_hold.py（読むだけの点検）
- rpc 行 send_angle を pad の move（t_send ≤ rpc の時刻 + 0.2 秒でいちばん新しいもの。送り直しの数まで同じ行に組む）と組にし、1 刻み（pad_step）は command を指令、長押し（pad_hold。no_check）は押した関節を見ない（15° 先へ着く気がない）。rpc 行 stop は halt（|t_halt − 時刻| ≤ 0.2 秒）と組にし、指令 = 直前の指令のうち止めた関節だけ measured_after_halt（gamepad_teleop の _apply_halt と同じ）。組にできない行は pad_unknown（値を出さない）
- rpc が 0.5 秒より短く返った区間（fast）では、動いている途中の遅れを数えず、代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」（away）を出し、4° を超えたら外れ。止まったあとのずれは、その関節が動いたなら動き出したあとの止まりだけで見る
- 11-01 の 9 本は、前の結果（docs/diag_joint_hold_20261101.json）と全部の値・区間ごとの一覧が同じになることを確かめた（fast の区間が 0）
- 結果: docs/diag_joint_hold_20261106.txt・.json。外れは 185554 だけ（補正 2 の j1 の長押しの間に j4 が指令 −64.5 のまま −63.5 → −57.7。止まったあとのずれ 6.8°。4 区間）

### B. 補正データ（KIOXIA。git の外）
- pi05/build_correction_episodes.py は変えていない（ゲームパッドの区間も correct として入り、爪の指令は gripper_cmd 行の時刻で切り替わり、止まりは同じ決まりで詰まることをテストで確かめた）
- `datasets/mecharm_redblock_box_corr_v5`（`--gap-fill speed20 --trim-hold`、corr_v4b の 7 本 + 183926・184452・184650・184906。前の 7 本は corr_v4b と episode.npz がバイトで同じ）と、trim2 と合わせた `..._teach_v1_trim2_corr_v5`（30 本 / 4301 コマ・tar 117 MB・`._` なし）
- テスト: pi05/test_pad_build.py（新）

### C. 続きの学習 corr3
- `lab_lib.sh`: lab_model_def に corr3（PixelstoMotors/pi05-mecharm-trim-corr-lora-v3・/workspace/ckpt/corr_v3_1499・設定 pi05_mecharm_trim_corr_v5_lora・データ ..._trim2_corr_v5）、lab_train_def に corr3（続きの元 corr2・variant v5・KIOXIA の由来 pi05_mecharm_trim_corr_lora_v2/1499）、lab_cmd_addon_model に `--variant v5 --steps 1500 --init /workspace/ckpt/corr_v2_1499/params`
- `pi05/openpi_addon/add_trim_corr_config.py`: VARIANTS に v5（old_tags なし）
- `pi05/openpi_addon/pod_prep_trim_corr.sh`: v5（NAME ..._corr_v5・N_MAC 30・NORM_SRC は corr_v2_1499 の assets の ..._corr_v4b/norm_stats.json。sha256 は trim2 と同じ 80346837…。KIOXIA の写しで確かめた）
- `lab_train.sh`・`lab_start.sh`: ヘルプとコメント（--help の行の範囲も合わせた）。`lab_train_fakes.sh`: v5 の偽物の tar
- テスト: test_lab_train.sh（12c）・test_lab_start.sh（34c と知らない名前の文言）・pi05/test_merge_and_config.py（v5 の設定・pod_prep v5・KIOXIA の corr_v5）・pi05/test_diag_joint_hold.py（ゲームパッドと fast の 6 件）

差分は `git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py lab_lib.sh lab_start.sh lab_train.sh lab_train_fakes.sh pi05/openpi_addon/ pi05/test_merge_and_config.py test_lab_start.sh test_lab_train.sh` と新しいファイル `pi05/test_pad_build.py` で見られます（ほかの変更は別の作業のものなので対象外）。

## 見てほしい点
1. diag_joint_hold の pad の組み方（_pad_send_for・送り直しの数え方・stop の指令の作り方）が gamepad_teleop.py の送り方（send_and_check / halt / _apply / _apply_halt）と食い違わないか。組にし違えて別の目標を指令にする場合がないか
2. fast の区間の決まり（away・動き出す前の止まりを使わない）で、11-01 の記録の結果が変わらないこと、手首が指令なしに垂れたものを見落とさないこと。逆に、ふつうの動きを外れにしないか
3. corr3 の学習（lab_train.sh --model corr3）で、送る tar・Pod で広げるデータ・設定の repo_id・正規化の置き場所・続きの元（corr2 の 1499 を /workspace/ckpt/corr_v2_1499 に取る）・上げる先（pi05-mecharm-trim-corr-lora-v3）・KIOXIA の置き場（checkpoints/pi05_mecharm_trim_corr_lora_v3/1499）・由来がそろうか。trim・corr・corr2 の動きが変わっていないか
4. lab_start.sh --model corr3（学習後の推論）で、assets の写し元 `corr_v3_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v5` が学習で openpi が書く名前と合うか
5. 推論の Pod（lab/Log22/pod/lab_pod.json。18:58 に消した記録）と、学習の Pod の記録（lab/Log22/train/lab_pod.json）がぶつからないか
6. データの扱い: 184650 は j2 が trim2 の範囲（上限 +31.4°）を最大 11° 超える 18 コマを含む（補正 2 の終わり）。今日の回は 1 コマの最大の動きが model で 4.8°・ゲームパッドの長押しで 6.7°（trim2 は 3.3° まで）。学習に入れることでの問題の見立て

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

#### 1回目の結果 docs/review_result_20261106_corr_v5_gamepad_codex.md

```markdown
# codex レビュー結果：ゲームパッドの点検・corr_v5・corr3（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad.md（codex exec -s read-only）

**corr3 の学習・推論の向け先は整合しています。ただし、診断コードには必須修正が 2 点あります。** 今回の「4 本を採用・185554 を除外」という結果は再現しましたが、「手首の垂れを見落とさない」とはまだ断定できません。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| pad 送信の対応付け | **必須** | [_pad_send_for](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:127) は `ts + 0.2` 以前の最新送信を選ぶため、次の送信を拾えます。送信 A=1.00 秒、B=1.15 秒、rpc=1.00 秒で **B を選ぶ**ことを再現しました。初回は時刻差が最小の未使用送信に対応させ、再送は対応済み送信の残り回数として扱い、曖昧なら unknown にしてください。今回の 5 本では未来送信との対応はありませんでした。 |
| fast の区間をまたぐ垂れ | **必須** | [away の計算](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:222) が区間ごとにリセットされます。j4 の指令が同じでも、別関節の送信で区切られ、実測が `0→3°`、次区間で `3→6°` と動き続けると、両区間とも away=3°・hold_err=NaN となり、6° の垂れを見逃します。**関節ごとに、指令が変わらない間の最小誤差を引き継ぐ**必要があります。再送・爪操作でも同様です。 |
| 再送の数え方 | 据え置き | `n_used > resends` の条件は初回 1 回＋再送数を許すため、回数に off-by-one はありません。ただし上記の対応付けを誤ると回数管理も崩れます。 |
| stop の指令復元 | 据え置き／推奨 | 既知の halt_joint だけ measured_after_halt に替える処理は `_apply_halt` と一致します。一方、halt の検索は範囲内の最初の行を選び、消費済み管理がありません。最近傍・一対一対応にすることを推奨します。 |
| fast の通常動作の扱い | 据え置き | 指令へ単調に近づく動きを lag として数えない方針は妥当です。動き出す前の止まりを除く処理も意図と一致します。ただし、away は原因を区別しないため、4°超のオーバーシュートも「外れ」になります。 |
| 11-01 との互換性 | 据え置き：確認済み | 9 本すべて fast=0。既存 JSON にある全結果項目が一致しました。 |
| 11-06 の選別結果 | 据え置き：再現済み | 5 本とも pad_unknown=0。採用する 4 本は events なし。185554 は j4 の外れが 4 区間で、最大約 6.8°。ただし上記の検出漏れを直した後の再点検が必要です。 |
| corr3 の学習経路 | 据え置き：整合 | [モデル定義](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_lib.sh:306)から、v5 tar → Pod の v5 データ → `pi05_mecharm_trim_corr_v5_lora`／`local/..._trim2_corr_v5` へ一致。初期重みは corr2 を `/workspace/ckpt/corr_v2_1499` に取得し、その params を使用します。 |
| 出力・由来 | 据え置き：整合 | HF 出力は `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3`、KIOXIA は `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499`。由来の入力も corr2 の `..._v2/1499`、データも v5 です。既存 trim・corr・corr2 の分岐に動作変更は見つかりませんでした。 |
| 正規化と推論 assets | 据え置き：整合 | corr2 の v4b assets にある正規化ファイルの SHA256 は指定値と一致。v5 の repo_id が既定 asset_id となるため、保存先は `assets/local/..._trim2_corr_v5` で、corr3 推論のコピー元と一致します。[固定コミットの設定実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/config.py)、[保存実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/checkpoints.py) |
| Log22 の Pod 記録 | 据え置き：分離済み | 推論は `Log22/pod/lab_pod.json`、学習は `Log22/train/lab_pod.json` で衝突しません。推論側には 18:58:28 の終了記録があります。確認時点では train ディレクトリは未作成でした。 |
| データの構成 | 据え置き：確認済み | 統合版は 30 本・4301 コマ。旧 7 本の episode.npz はバイト一致。tar 内も 30 本で `._` はありません。 |
| 184650 の範囲超過 | **推奨：評価条件に明記** | 実データの j2 上限は trim2=31.37°、v5=42.45°。推論側は trim2＋10°なので上限約41.37°となり、**学習軌道の末端約1.08°はクランプされます**。追加自体を禁止する理由にはなりませんが、同じ軌道を完全再現できない条件として扱ってください。 |
| 4.8°／6.7° のコマ間移動 | 推奨 | 従来より大きい動きを教師にするため、出力の移動量や補正頻度が増える可能性があります。正規化の維持は妥当ですが、成功率だけでなくクランプ・移動量制限の発生数も比較すると判断できます。1コマ差に加え、学習が使うチャンク先頭からの差分も確認すると有用です。 |
| 学習テストの assets | 推奨 | [偽物チェックポイント](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_train_fakes.sh:72)は v3 assets 固定です。corr3 では v5 に切り替え、取得した assets 名まで検査すると学習→推論間の不整合を検出できます。 |

必須は **送信の対応付け**と**指令不変の関節で away を区間越しに保持する処理**です。この修正後に 5 本を再点検してから採用を確定するのが適切です。

読み取り専用の実ログテスト 2 件は成功しました。一時ファイルを生成するテスト群は未実行です。ファイル変更、実機・Pod・Hugging Face の操作は行っていません。
```

#### 2回目の依頼文 docs/review_request_20261106_corr_v5_gamepad_r2.md

```markdown
# コードレビュー依頼 R2：diag_joint_hold の pad の組み方と away の引き継ぎ（2026-11-06 依頼 97）

R1（docs/review_result_20261106_corr_v5_gamepad_codex.md）の必須 2 件と推奨 1 件を pi05/diag_joint_hold.py に反映した。

1. 必須（pad 送信の対応付け）: `_pad_send_for(sends, used, ts)` を、まだ組にしていない送信のうち |t_send − ts| ≤ 0.2 秒でいちばん近いものを初めての送信とし、なければ ts より前に組にした送信のうちいちばん新しいものの送り直し（resends の数まで）とする形にした。どちらもなければ pad_unknown
2. 必須（区間をまたぐ垂れ）: すぐ返った区間がつづき、その関節の指令が変わらない間は、関節ごとに最小の |実測−指令| を区間をまたいで引き継ぐ（指令が変わる・すぐ返らない区間・長押しの関節では引き継がない）
3. 推奨（stop の対応）: halt もいちばん近い・まだ組にしていないものと一対一で組む

テスト: pi05/test_diag_joint_hold.py に 2 件（次の送信を拾わない・0→3° と 3→6° の垂れを 6° と見る／指令が変われば引き継がない）。21 件 OK。
11-01 の 9 本は前の結果と全部同じ。11-06 の 5 本: 入れる 4 本は外れ 0（j4 の離れた分の最大 3.8°（184452））、185554 は外れ 5 区間（j4）。docs/diag_joint_hold_20261106.txt・.json を作り直した。

差分は `git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py` で見られます。
ほかの推奨（184650 の範囲・1 コマの動き・偽物の assets）は報告に書いて据え置きます。

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

#### 2回目の結果 docs/review_result_20261106_corr_v5_gamepad_codex_r2.md

````markdown
# codex レビュー結果 R2：pad の組み方と away の引き継ぎ（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad_r2.md（codex exec -s read-only）

差分、R1の指摘、テストと再生成レポートを読み取りだけで確認します。pad の対応付けと関節ごとの引き継ぎ条件を中心にレビューします。
exec
/bin/zsh -lc 'git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 127ms:
git: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
git: error: couldn't create cache file '/tmp/xcrun_db-4DMDrhxj' (errno=Operation not permitted)
git: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
git: error: couldn't create cache file '/tmp/xcrun_db-YIBxMFZR' (errno=Operation not permitted)
diff --git a/pi05/diag_joint_hold.py b/pi05/diag_joint_hold.py
index b9f6122..0200f31 100644
--- a/pi05/diag_joint_hold.py
+++ b/pi05/diag_joint_hold.py
@@ -23,6 +23,26 @@
     動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
   - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.
 
+ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
+  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
+    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
+    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
+      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
+      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
+                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
+      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
+                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
+    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
+  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
+    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
+      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
+      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
+        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
+        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
+      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
+        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
+    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)
+
 log で見る (依頼 46 より前の記録とも比べられる):
   - 補正の 1 刻み: measured_after − after (全 6 関節)
   - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)
@@ -49,6 +69,10 @@ LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いてい
 # 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
 # Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
 EVENT_DEG = 4.0
+# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
+# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
+FAST_RPC_SEC = 0.5
+PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限
 
 
 def load_rows(path):
@@ -81,22 +105,94 @@ def _rpc_sec(r):
     return 0.0
 
 
-def segments(rows):
-    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), ts[], q[], stages[]}]"""
+def pad_events(log):
+    """log.jsonl の type=pad の行から, 送ったもの (move / home) と止めたもの (halt) を時刻の順に集める (依頼 97)."""
+    sends, halts = [], []
+    for r in log or ():
+        if r.get("type") != "pad":
+            continue
+        if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None \
+                and r.get("t_send") is not None:
+            sends.append({"t": float(r["t_send"]), "cmd": np.asarray(r["command"][:6], float),
+                          "joint": r.get("joint") if r.get("kind") == "move" else None,
+                          "hold": bool(r.get("no_check")), "resends": len(r.get("resends") or []),
+                          "corr": r.get("correction_id")})
+        elif r.get("kind") == "halt" and r.get("t_halt") is not None:
+            q = r.get("measured_after_halt")
+            halts.append({"t": float(r["t_halt"]), "q": None if q is None else np.asarray(q[:6], float),
+                          "joint": r.get("halt_joint"), "how": r.get("halt"), "corr": r.get("correction_id")})
+    sends.sort(key=lambda x: x["t"]); halts.sort(key=lambda x: x["t"])
+    return sends, halts
+
+
+def _pad_send_for(sends, used, ts):
+    """send_angle の rpc 行 (時刻 ts) に当たる pad の送信と, それが何回目か (0 = 初めて・1 以上 = 送り直し).
+    codex R1 必須: 初めての送信は, まだ組にしていない送信のうち |t_send − ts| が PAD_MATCH_SEC 以内でいちばん近いもの.
+    それがなければ, ts より前に組にした送信のうち送り直しの残りがあるいちばん新しいものの送り直しとする.
+    どちらもなければ (None, 0). used = {送信の番号: 組にした回数}"""
+    cand = [(abs(x["t"] - ts), k) for k, x in enumerate(sends) if k not in used and abs(x["t"] - ts) <= PAD_MATCH_SEC]
+    if cand:
+        k = min(cand)[1]
+        used[k] = 1
+        return sends[k], 0
+    for k in sorted((k for k in used if sends[k]["t"] <= ts), key=lambda k: sends[k]["t"], reverse=True):
+        if used[k] <= sends[k]["resends"]:
+            used[k] += 1
+            return sends[k], used[k] - 1
+        break               # いちばん新しい送信の送り直しが尽きていれば, それより前のものにはしない
+    return None, 0
+
+
+def segments(rows, log=None):
+    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), fast, skip, ts[], q[], stages[]}]
+    log (log.jsonl の行) があれば, ゲームパッドの rpc 行 (send_angle / stop) の指令を pad の行から作る (依頼 97)."""
+    sends, halts = pad_events(log)
+    used_sends, used_halts = {}, set()
     segs, cur = [], None
+    nan6 = np.full(6, np.nan)
     for r in rows:
         ev = r.get("event_type")
         if ev == "send_angles":
             q = _q(r)
             if q is None:
                 continue
+            rs = _rpc_sec(r)
             cur = {"kind": "send", "stage": r.get("stage"), "cmd": q, "t_cmd": r["_ts"],
-                   "t0": r["_ts"] + _rpc_sec(r), "ts": [], "q": [], "stages": []}
+                   "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC, "skip": None,
+                   "ts": [], "q": [], "stages": []}
             segs.append(cur)
         elif ev == "rpc" and cur is not None:
             cmd = (r.get("note") or "").split()[0].replace("cmd=", "")
-            cur = {"kind": "grip_after", "grip_cmd": cmd, "stage": r.get("stage"), "cmd": cur["cmd"],
-                   "t_cmd": cur["t_cmd"], "t0": r["_ts"] + _rpc_sec(r), "ts": [], "q": [], "stages": []}
+            rs = _rpc_sec(r)
+            base = {"stage": r.get("stage"), "t_cmd": r["_ts"], "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC,
+                    "skip": None, "ts": [], "q": [], "stages": []}
+            if cmd == "send_angle":
+                x, n_used = _pad_send_for(sends, used_sends, r["_ts"])
+                if x is None:
+                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
+                else:
+                    cur = dict(base, kind="pad_hold" if x["hold"] else "pad_step", cmd=x["cmd"].copy(),
+                               skip=x["joint"] if x["hold"] else None, t_cmd=x["t"] if n_used == 0 else r["_ts"])
+            elif cmd == "stop":
+                # codex R1 推奨: いちばん近い・まだ組にしていない halt (一対一)
+                hc = [(abs(x["t"] - r["_ts"]), k) for k, x in enumerate(halts)
+                      if k not in used_halts and abs(x["t"] - r["_ts"]) <= PAD_MATCH_SEC]
+                h = None
+                if hc:
+                    used_halts.add(min(hc)[1])
+                    h = halts[min(hc)[1]]
+                if h is None or h["q"] is None:
+                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
+                else:
+                    c = np.asarray(cur["cmd"], float).copy()
+                    if h["joint"] is not None and not np.isnan(c).any():
+                        c[int(h["joint"])] = h["q"][int(h["joint"])]
+                    else:
+                        c = h["q"].copy()
+                    cur = dict(base, kind="pad_stop", cmd=c)
+            else:
+                cur = dict(base, kind="grip_after", grip_cmd=cmd, cmd=cur["cmd"], t_cmd=cur["t_cmd"],
+                           fast=cur.get("fast", False))
             segs.append(cur)
         elif ev == "actual" and cur is not None:
             q = _q(r)
@@ -123,17 +219,44 @@ def analyze_segments(segs):
     戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
     out = []
     nan6 = np.full(6, np.nan)
+    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
+    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
+    carry_cmd, carry_min = nan6.copy(), nan6.copy()
     for k, s in enumerate(segs):
         n = len(s["ts"])
         rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
                "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
                "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
-        if n:
+        rec["away_max"] = np.zeros(6)
+        rec["skip"] = s.get("skip")
+        rec["fast"] = bool(s.get("fast"))
+        if n and not np.isnan(s["cmd"]).any():
             err = s["q"] - s["cmd"]
             mv = moving_mask(s["ts"], s["q"])
-            rec["lag_max"] = np.where(mv.any(axis=0), np.where(mv, np.abs(err), 0).max(axis=0), 0.0)
+            if s.get("fast"):
+                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
+                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
+                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
+                a = np.abs(err)
+                same = np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0) & ~np.isnan(carry_min)
+                start = np.where(same, np.fmin(carry_min, a[0]), a[0])
+                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
+                rec["away_max"] = np.max(a - run_min, axis=0)
+                carry_cmd, carry_min = s["cmd"].copy(), run_min[-1].copy()
+                if s.get("skip") is not None:
+                    carry_min[int(s["skip"])] = np.nan
+                lag_mv = np.zeros_like(mv)
+            else:
+                lag_mv = mv
+                carry_cmd, carry_min = nan6.copy(), nan6.copy()
+            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
             for i in range(6):
+                if s.get("skip") is not None and i == int(s["skip"]):
+                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                 idx = np.flatnonzero(~mv[:, i])
+                if s.get("fast") and mv[:, i].any():
+                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
+                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                 if not len(idx):
                     continue
                 last = idx[-1]
@@ -143,7 +266,15 @@ def analyze_segments(segs):
                 rec["hold_err"][i] = err[last, i]
                 rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                 rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
-            rec["coarse_max"] = np.abs(err).max(axis=0)
+            if s.get("skip") is not None:
+                rec["lag_max"][int(s["skip"])] = 0.0
+                rec["away_max"][int(s["skip"])] = 0.0
+            ce = np.abs(err)
+            if s.get("skip") is not None:
+                ce[:, int(s["skip"])] = 0.0
+            rec["coarse_max"] = ce.max(axis=0)
+            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
+        elif n:
             rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
         out.append(rec)
     return out
@@ -200,7 +331,7 @@ def summarize_run(run_dir):
                 pass
     res = {"run": stem, "outcome": trial.get("outcome"), "corrections": trial.get("corrections")}
     rows = load_rows(run_dir / "recording" / "states.csv")
-    segs = analyze_segments(segments(rows))
+    segs = analyze_segments(segments(rows, log))
     res["has_send_rows"] = bool(segs)
     lh = log_hold_errors(log)
     res["log"] = {k: {j: _stats([e[i] for _, e in v]) for i, j in enumerate(JOINTS)} for k, v in lh.items()}
@@ -235,13 +366,21 @@ def summarize_run(run_dir):
     for s in work:
         for i, j in enumerate(JOINTS):
             he = float(s["hold_err"][i]) if _has(s["hold_err"][i]) else None
-            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG:
-                ev.append({"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
-                           "joint": j, "cmd": round(float(s["cmd"][i]), 2),
-                           "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
-                           "hold_err": None if he is None else round(he, 2),
-                           "lag_max": round(float(s["lag_max"][i]), 2)})
+            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG or s["away_max"][i] > EVENT_DEG:
+                e = {"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
+                     "joint": j, "cmd": round(float(s["cmd"][i]), 2),
+                     "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
+                     "hold_err": None if he is None else round(he, 2),
+                     "lag_max": round(float(s["lag_max"][i]), 2)}
+                if s["away_max"][i] > 0:
+                    e["away_max"] = round(float(s["away_max"][i]), 2)      # 依頼 97 (送ったらすぐ返った区間だけ)
+                ev.append(e)
     res["events"] = ev
+    # 依頼 97: 送ったらすぐ返った区間の数・ゲームパッドの区間の数・組にできなかった rpc 行の数
+    res["fast_segments"] = sum(1 for s in segs if s.get("fast"))
+    res["pad_segments"] = {k: sum(1 for s in segs if s["kind"] == k) for k in ("pad_step", "pad_hold", "pad_stop",
+                                                                             "pad_unknown")}
+    res["away_max"] = {j: round(float(max([s["away_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}
     res["event_segments"] = len({e["seg"] for e in ev})   # codex R1・R2 推奨: 区間の数 (区間の番号で数える. 関節は重ねない)
     res["drift_max"] = {j: round(float(max([abs(s["drift"][i]) for s in work
                                             if _has(s["drift"][i]) and s["still_sec"][i] >= LONG_HOLD_SEC] or [0.0])), 2)
@@ -290,7 +429,7 @@ def main():
     if str(root).startswith("/Volumes/KIOXIA") and not Path("/Volumes/KIOXIA").is_mount():
         sys.exit("[ABORT] KIOXIA not mounted: /Volumes/KIOXIA")
     out = {"params": {"move_win_sec": MOVE_WIN_SEC, "move_eps_deg": MOVE_EPS_DEG, "event_deg": EVENT_DEG,
-                      "long_hold_sec": LONG_HOLD_SEC}, "runs": {}, "timelines": {}}
+                      "long_hold_sec": LONG_HOLD_SEC, "fast_rpc_sec": FAST_RPC_SEC}, "runs": {}, "timelines": {}}
     for stem in args.run:
         res, segs = summarize_run(root / stem)
         out["runs"][stem] = res
@@ -308,11 +447,15 @@ def main():
             print("   止まり中央 " + _h("median"))
             print("   止まり符号 " + " ".join(f"{j}={'-' if v is None else f'{v:+.1f}':>5}" for j, v in (res["hold_signed_median"] or {}).items()))
             print("   じわじわ   " + " ".join(f"{j}={v:5.1f}" for j, v in res["drift_max"].items()))
+            if res["fast_segments"]:
+                print("   向かう途中に離れた " + " ".join(f"{j}={v:5.1f}" for j, v in res["away_max"].items())
+                      + f"  (すぐ返った区間 {res['fast_segments']}・ゲームパッド {res['pad_segments']})")
             print(f"   段ごと {res['hold_by_context']}")
             print(f"   外れ (>{EVENT_DEG:.0f}°) {res['event_segments']} 区間 ({len(res['events'])} 区間・関節の組)")
             for e in res["events"]:
                 print(f"     {e['sec']:6.1f}秒 {e['stage']:7s} {e['kind']:10s} {e['joint']} 指令 {e['cmd']:7.2f} "
-                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}")
+                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}"
+                      + (f"  離れた {e['away_max']}" if "away_max" in e else ""))
             for gr in res["grip"]:
                 print(f"   爪 {gr['cmd']} ({gr['stage']}) 前後の変化 {gr['delta']}  爪のあとのずれ {gr['err_after']}")
             for h in res["home"]:
diff --git a/pi05/test_diag_joint_hold.py b/pi05/test_diag_joint_hold.py
index 2efe3cb..512cbe8 100644
--- a/pi05/test_diag_joint_hold.py
+++ b/pi05/test_diag_joint_hold.py
@@ -6,6 +6,7 @@
   - 爪の rpc で区間を切る
   - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
   - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
+  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
 """
 import csv
 import json
@@ -33,6 +34,9 @@ class Rec:
     def send(self, t, q, stage="model", rpc=1.5):
         self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})
 
+    def rpc(self, t, cmd, stage="correct", rpc=0.03):
+        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})
+
     def grip(self, t, stage="model", rpc=1.5):
         self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})
 
@@ -223,6 +227,149 @@ class TestSegments(unittest.TestCase):
         self.assertEqual(segs, [])
 
 
+def pad_move(t, command, joint, hold=False, resends=(), cid=1):
+    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
+         "joint": joint, "resends": list(resends)}
+    if hold:
+        r["no_check"] = True
+    return r
+
+
+def pad_halt(t, q, joint, cid=1):
+    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
+            "halt_joint": joint, "halt": "stop"}
+
+
+class TestPadAndFast(unittest.TestCase):
+    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""
+
+    def build(self, rec, log=()):
+        td = tempfile.mkdtemp()
+        run = Path(td) / "pi05_p"
+        rec.write(run, log)
+        return d.summarize_run(run)
+
+    def test_fast_approach_is_not_lag(self):
+        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
+        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
+        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
+        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
+        r.actual(2.3, 3.5, pose(-59.5))
+        res, segs = self.build(r)
+        self.assertEqual(res["fast_segments"], 1)
+        self.assertEqual(res["events"], [])
+        self.assertEqual(res["lag_max"]["j4"], 0.0)
+        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
+        self.assertLess(res["away_max"]["j4"], 0.2)
+
+    def test_fast_hold_ignores_still_before_moving(self):
+        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 0.8, pose(-50))
+        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
+        res, segs = self.build(r)
+        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
+        self.assertEqual(res["events"], [])
+
+    def test_fast_sag_after_arrival_is_event(self):
+        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 1.0, pose(-60))
+        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
+        r.actual(1.6, 3.0, pose(-54))
+        res, segs = self.build(r)
+        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
+        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
+        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)
+
+    def test_pad_step_hold_stop(self):
+        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
+        start = pose(-60, j1=40)
+        step = pose(-60, j1=40); step[0] = 1.0
+        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
+        r = Rec()
+        r.send(0.0, start, stage="model", rpc=0.03)
+        r.actual(0.03, 1.0, start, stage="model")
+        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
+        r.actual(2.03, 3.0, step, stage="correct")
+        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)
+        r.actual(3.03, 3.5, lambda t: [1.0, 40 - 20 * (t - 3.03), 0, 0, -60, -30], stage="correct")
+        r.rpc(3.5, "stop")
+        r.actual(3.55, 5.0, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
+        r.rpc(5.0, "send_angle")                                               # 組にできない (pad の行がない… 送り直しの数を越える)
+        r.actual(5.03, 5.5, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
+        log = [pad_move(2.0, step, 0), pad_move(3.0, far, 1, hold=True), pad_halt(3.5, [1.0, 30.6, 0, 0, -60.2, -30], 1)]
+        res, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_step", "pad_hold", "pad_stop", "pad_unknown"])
+        self.assertEqual(res["pad_segments"], {"pad_step": 1, "pad_hold": 1, "pad_stop": 1, "pad_unknown": 1})
+        np.testing.assert_allclose(segs[1]["cmd"], step)
+        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
+        self.assertEqual(segs[2]["lag_max"][1], 0.0)
+        self.assertEqual(segs[2]["skip"], 1)
+        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
+        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
+        self.assertEqual(res["events"], [])
+
+    def test_pad_resend_and_uncommanded_wrist(self):
+        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
+        start = pose(-60, j1=50)
+        far = pose(-60, j1=35)
+        r = Rec()
+        r.send(0.0, start, rpc=0.03)
+        r.actual(0.03, 1.0, start)
+        r.rpc(2.0, "send_angle")
+        r.rpc(2.3, "send_angle")                                               # 送り直し
+        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
+        r.rpc(3.0, "stop")
+        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
+        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
+        res, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
+        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
+        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
+        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)
+
+    def test_pad_send_not_matched_to_next(self):
+        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
+        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
+        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
+        r = Rec()
+        r.send(0.0, pose(-60, j1=40), rpc=0.03)
+        r.actual(0.03, 0.9, pose(-60, j1=40))
+        r.rpc(1.0, "send_angle")
+        r.actual(1.03, 1.14, a_cmd, stage="correct")
+        r.rpc(1.16, "send_angle")
+        r.actual(1.19, 2.0, b_cmd, stage="correct")
+        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
+        _, segs = self.build(r, log)
+        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
+        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)
+
+    def test_away_carries_across_fast_segments(self):
+        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
+        r = Rec()
+        r.send(0.0, pose(-60, j1=10), rpc=0.03)
+        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
+        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
+        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
+        res, segs = self.build(r)
+        self.assertLess(segs[0]["away_max"][4], 3.1)
+        self.assertGreater(segs[1]["away_max"][4], 5.5)
+        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
+        r2 = Rec()                                          # j4 の指令が変われば引き継がない
+        r2.send(0.0, pose(-60), rpc=0.03)
+        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
+        r2.send(1.0, pose(-57), rpc=0.03)
+        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
+        res2, _ = self.build(r2)
+        self.assertEqual(res2["events"], [])
+
+
 RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")
 
 
@@ -242,6 +389,21 @@ class TestToday(unittest.TestCase):
             else:
                 self.assertGreater(n_ev, 0, stem)
 
+    def test_20261106_gamepad(self):
+        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
+        for stem in ("183926", "184452", "184650", "184906", "185554"):
+            p = RUNS / f"pi05_20261106_{stem}"
+            if not p.is_dir():
+                self.skipTest("11-06 の記録がない")
+            res, _ = d.summarize_run(p)
+            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
+            self.assertGreater(res["fast_segments"], 0, stem)
+            if stem == "185554":
+                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
+                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
+            else:
+                self.assertEqual(res["events"], [], stem)
+
 
 if __name__ == "__main__":
     unittest.main()

exec
/bin/zsh -lc 'cat docs/review_result_20261106_corr_v5_gamepad_codex.md; cat pi05/diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 77ms:
# codex レビュー結果：ゲームパッドの点検・corr_v5・corr3（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad.md（codex exec -s read-only）

**corr3 の学習・推論の向け先は整合しています。ただし、診断コードには必須修正が 2 点あります。** 今回の「4 本を採用・185554 を除外」という結果は再現しましたが、「手首の垂れを見落とさない」とはまだ断定できません。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| pad 送信の対応付け | **必須** | [_pad_send_for](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:127) は `ts + 0.2` 以前の最新送信を選ぶため、次の送信を拾えます。送信 A=1.00 秒、B=1.15 秒、rpc=1.00 秒で **B を選ぶ**ことを再現しました。初回は時刻差が最小の未使用送信に対応させ、再送は対応済み送信の残り回数として扱い、曖昧なら unknown にしてください。今回の 5 本では未来送信との対応はありませんでした。 |
| fast の区間をまたぐ垂れ | **必須** | [away の計算](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:222) が区間ごとにリセットされます。j4 の指令が同じでも、別関節の送信で区切られ、実測が `0→3°`、次区間で `3→6°` と動き続けると、両区間とも away=3°・hold_err=NaN となり、6° の垂れを見逃します。**関節ごとに、指令が変わらない間の最小誤差を引き継ぐ**必要があります。再送・爪操作でも同様です。 |
| 再送の数え方 | 据え置き | `n_used > resends` の条件は初回 1 回＋再送数を許すため、回数に off-by-one はありません。ただし上記の対応付けを誤ると回数管理も崩れます。 |
| stop の指令復元 | 据え置き／推奨 | 既知の halt_joint だけ measured_after_halt に替える処理は `_apply_halt` と一致します。一方、halt の検索は範囲内の最初の行を選び、消費済み管理がありません。最近傍・一対一対応にすることを推奨します。 |
| fast の通常動作の扱い | 据え置き | 指令へ単調に近づく動きを lag として数えない方針は妥当です。動き出す前の止まりを除く処理も意図と一致します。ただし、away は原因を区別しないため、4°超のオーバーシュートも「外れ」になります。 |
| 11-01 との互換性 | 据え置き：確認済み | 9 本すべて fast=0。既存 JSON にある全結果項目が一致しました。 |
| 11-06 の選別結果 | 据え置き：再現済み | 5 本とも pad_unknown=0。採用する 4 本は events なし。185554 は j4 の外れが 4 区間で、最大約 6.8°。ただし上記の検出漏れを直した後の再点検が必要です。 |
| corr3 の学習経路 | 据え置き：整合 | [モデル定義](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_lib.sh:306)から、v5 tar → Pod の v5 データ → `pi05_mecharm_trim_corr_v5_lora`／`local/..._trim2_corr_v5` へ一致。初期重みは corr2 を `/workspace/ckpt/corr_v2_1499` に取得し、その params を使用します。 |
| 出力・由来 | 据え置き：整合 | HF 出力は `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3`、KIOXIA は `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499`。由来の入力も corr2 の `..._v2/1499`、データも v5 です。既存 trim・corr・corr2 の分岐に動作変更は見つかりませんでした。 |
| 正規化と推論 assets | 据え置き：整合 | corr2 の v4b assets にある正規化ファイルの SHA256 は指定値と一致。v5 の repo_id が既定 asset_id となるため、保存先は `assets/local/..._trim2_corr_v5` で、corr3 推論のコピー元と一致します。[固定コミットの設定実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/config.py)、[保存実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/checkpoints.py) |
| Log22 の Pod 記録 | 据え置き：分離済み | 推論は `Log22/pod/lab_pod.json`、学習は `Log22/train/lab_pod.json` で衝突しません。推論側には 18:58:28 の終了記録があります。確認時点では train ディレクトリは未作成でした。 |
| データの構成 | 据え置き：確認済み | 統合版は 30 本・4301 コマ。旧 7 本の episode.npz はバイト一致。tar 内も 30 本で `._` はありません。 |
| 184650 の範囲超過 | **推奨：評価条件に明記** | 実データの j2 上限は trim2=31.37°、v5=42.45°。推論側は trim2＋10°なので上限約41.37°となり、**学習軌道の末端約1.08°はクランプされます**。追加自体を禁止する理由にはなりませんが、同じ軌道を完全再現できない条件として扱ってください。 |
| 4.8°／6.7° のコマ間移動 | 推奨 | 従来より大きい動きを教師にするため、出力の移動量や補正頻度が増える可能性があります。正規化の維持は妥当ですが、成功率だけでなくクランプ・移動量制限の発生数も比較すると判断できます。1コマ差に加え、学習が使うチャンク先頭からの差分も確認すると有用です。 |
| 学習テストの assets | 推奨 | [偽物チェックポイント](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_train_fakes.sh:72)は v3 assets 固定です。corr3 では v5 に切り替え、取得した assets 名まで検査すると学習→推論間の不整合を検出できます。 |

必須は **送信の対応付け**と**指令不変の関節で away を区間越しに保持する処理**です。この修正後に 5 本を再点検してから採用を確定するのが適切です。

読み取り専用の実ログテスト 2 件は成功しました。一時ファイルを生成するテスト群は未実行です。ファイル変更、実機・Pod・Hugging Face の操作は行っていません。
#!/usr/bin/env python3
"""diag_joint_hold.py — 実機推論の記録から, 関節ごとの「動いている途中の遅れ」と「止まったあとのずれ (垂れ)」を出す (2026-11-01 依頼 57).

読むだけ (runs/ には書かない). アームは使わない.

入力: runs/pi05_policy/{run_stem}/recording/states.csv と log.jsonl
  states.csv (依頼 46 のあとの記録) には send_angles 行 (送った時刻・目標・rpc_sec) と, 爪の rpc 行がある.
  依頼 46 より前の記録 (send_angles 行がない) は log.jsonl だけで見る (下の「log で見る」).

分け方 (states.csv で見る):
  - 指令 = 直前の send_angles の目標. 送ってから rpc が返るまで (daemon が着いたと判定するまで. 約 1.6 秒) は
    つながりがふさがり, 実測は残らない. rpc が返ったあとから次の送信までを「止まっているはずの区間」(hold) とする.
    爪の rpc (開け閉め. 指令は変わらない) でも区間を切る (爪の前・爪の後 を別に見る).
  - 区間の中のコマごと・関節ごとに, 前後 MOVE_WIN_SEC 以内の実測のふれが MOVE_EPS_DEG を超えていれば「動いている」.
    動いているコマの |実測 − 指令| を「動いている途中の遅れ」とする (着いたと判定されたあとも動いていた分も含む).
  - 動いていないコマの |実測 − 指令| を「止まったあとのずれ」とする. 関節ごとに, 区間の最後の動いていないコマの値を
    その区間のずれとし (区間の数で数える. 長く待った区間が重くならない), run ごとに中央値・90%・最大を出す.
    codex R1 必須: 動いているかは関節ごとに見る (j1 が動き続けていても, 止まった j4 のずれを見落とさない).
    その関節が区間の中で一度も止まっていなければ, その区間のずれは無し (NaN) とする.
  - 区間の最後の「止まりが続いたところ」(関節ごと. つながった動いていないコマ) の変化 (最後 − 最初) を
    「止まっている間の変化」(じわじわ垂れる) とし, 続いた秒も出す. MOVE_EPS_DEG に満たない速さで動き続けるものはここに出る.
  - 前後 0.3 秒の窓で動きが見えなかったコマを「動いていない」としている (区間の端・まばらな記録では 1 コマでも
    動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
  - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.

ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)

log で見る (依頼 46 より前の記録とも比べられる):
  - 補正の 1 刻み: measured_after − after (全 6 関節)
  - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)

Usage:
  python3 pi05/diag_joint_hold.py --run pi05_20261101_190347 --run pi05_20261101_194042
  python3 pi05/diag_joint_hold.py --run ... --json docs/diag_joint_hold_20261101.json
"""
import argparse
import csv
import json
import sys
from pathlib import Path

import numpy as np

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
RUNS_ROOT = DATA_ROOT / "runs" / "pi05_policy"
JOINTS = ["j0", "j1", "j2", "j3", "j4", "j5"]
MOVE_WIN_SEC = 0.3      # 前後これだけの間の実測のふれで「動いている」を見る
MOVE_EPS_DEG = 0.3      # ふれがこれを超えたら動いている (読みの刻みは約 0.09°)
LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いていないコマがこれだけ続いた区間で見る
# 指令が変わらないのに動いた・止まったあとも残ったずれがこれを超えた区間を「外れ」として数える (依頼 57).
# 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
# Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
EVENT_DEG = 4.0
# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
FAST_RPC_SEC = 0.5
PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限


def load_rows(path):
    rows = []
    with Path(path).open(newline="") as f:
        for r in csv.DictReader(f):
            try:
                r["_ts"] = float(r["ts"])
            except Exception:
                continue
            rows.append(r)
    rows.sort(key=lambda r: r["_ts"])
    return rows


def _q(r):
    try:
        return np.array([float(r[j]) for j in JOINTS])
    except Exception:
        return None


def _rpc_sec(r):
    for tok in (r.get("note") or "").split():
        if tok.startswith("rpc_sec="):
            try:
                return float(tok.split("=", 1)[1])
            except ValueError:
                return 0.0
    return 0.0


def pad_events(log):
    """log.jsonl の type=pad の行から, 送ったもの (move / home) と止めたもの (halt) を時刻の順に集める (依頼 97)."""
    sends, halts = [], []
    for r in log or ():
        if r.get("type") != "pad":
            continue
        if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None \
                and r.get("t_send") is not None:
            sends.append({"t": float(r["t_send"]), "cmd": np.asarray(r["command"][:6], float),
                          "joint": r.get("joint") if r.get("kind") == "move" else None,
                          "hold": bool(r.get("no_check")), "resends": len(r.get("resends") or []),
                          "corr": r.get("correction_id")})
        elif r.get("kind") == "halt" and r.get("t_halt") is not None:
            q = r.get("measured_after_halt")
            halts.append({"t": float(r["t_halt"]), "q": None if q is None else np.asarray(q[:6], float),
                          "joint": r.get("halt_joint"), "how": r.get("halt"), "corr": r.get("correction_id")})
    sends.sort(key=lambda x: x["t"]); halts.sort(key=lambda x: x["t"])
    return sends, halts


def _pad_send_for(sends, used, ts):
    """send_angle の rpc 行 (時刻 ts) に当たる pad の送信と, それが何回目か (0 = 初めて・1 以上 = 送り直し).
    codex R1 必須: 初めての送信は, まだ組にしていない送信のうち |t_send − ts| が PAD_MATCH_SEC 以内でいちばん近いもの.
    それがなければ, ts より前に組にした送信のうち送り直しの残りがあるいちばん新しいものの送り直しとする.
    どちらもなければ (None, 0). used = {送信の番号: 組にした回数}"""
    cand = [(abs(x["t"] - ts), k) for k, x in enumerate(sends) if k not in used and abs(x["t"] - ts) <= PAD_MATCH_SEC]
    if cand:
        k = min(cand)[1]
        used[k] = 1
        return sends[k], 0
    for k in sorted((k for k in used if sends[k]["t"] <= ts), key=lambda k: sends[k]["t"], reverse=True):
        if used[k] <= sends[k]["resends"]:
            used[k] += 1
            return sends[k], used[k] - 1
        break               # いちばん新しい送信の送り直しが尽きていれば, それより前のものにはしない
    return None, 0


def segments(rows, log=None):
    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), fast, skip, ts[], q[], stages[]}]
    log (log.jsonl の行) があれば, ゲームパッドの rpc 行 (send_angle / stop) の指令を pad の行から作る (依頼 97)."""
    sends, halts = pad_events(log)
    used_sends, used_halts = {}, set()
    segs, cur = [], None
    nan6 = np.full(6, np.nan)
    for r in rows:
        ev = r.get("event_type")
        if ev == "send_angles":
            q = _q(r)
            if q is None:
                continue
            rs = _rpc_sec(r)
            cur = {"kind": "send", "stage": r.get("stage"), "cmd": q, "t_cmd": r["_ts"],
                   "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC, "skip": None,
                   "ts": [], "q": [], "stages": []}
            segs.append(cur)
        elif ev == "rpc" and cur is not None:
            cmd = (r.get("note") or "").split()[0].replace("cmd=", "")
            rs = _rpc_sec(r)
            base = {"stage": r.get("stage"), "t_cmd": r["_ts"], "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC,
                    "skip": None, "ts": [], "q": [], "stages": []}
            if cmd == "send_angle":
                x, n_used = _pad_send_for(sends, used_sends, r["_ts"])
                if x is None:
                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
                else:
                    cur = dict(base, kind="pad_hold" if x["hold"] else "pad_step", cmd=x["cmd"].copy(),
                               skip=x["joint"] if x["hold"] else None, t_cmd=x["t"] if n_used == 0 else r["_ts"])
            elif cmd == "stop":
                # codex R1 推奨: いちばん近い・まだ組にしていない halt (一対一)
                hc = [(abs(x["t"] - r["_ts"]), k) for k, x in enumerate(halts)
                      if k not in used_halts and abs(x["t"] - r["_ts"]) <= PAD_MATCH_SEC]
                h = None
                if hc:
                    used_halts.add(min(hc)[1])
                    h = halts[min(hc)[1]]
                if h is None or h["q"] is None:
                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
                else:
                    c = np.asarray(cur["cmd"], float).copy()
                    if h["joint"] is not None and not np.isnan(c).any():
                        c[int(h["joint"])] = h["q"][int(h["joint"])]
                    else:
                        c = h["q"].copy()
                    cur = dict(base, kind="pad_stop", cmd=c)
            else:
                cur = dict(base, kind="grip_after", grip_cmd=cmd, cmd=cur["cmd"], t_cmd=cur["t_cmd"],
                           fast=cur.get("fast", False))
            segs.append(cur)
        elif ev == "actual" and cur is not None:
            q = _q(r)
            if q is None or r["_ts"] < cur["t0"] - 0.005:
                continue
            cur["ts"].append(r["_ts"]); cur["q"].append(q); cur["stages"].append(r.get("stage"))
    for s in segs:
        s["ts"] = np.array(s["ts"]); s["q"] = np.array(s["q"]).reshape(-1, 6)
    return segs


def moving_mask(ts, q, win=MOVE_WIN_SEC, eps=MOVE_EPS_DEG):
    """(n, 6) 関節ごとに, 前後 win 秒以内の実測のふれが eps を超えるか."""
    n = len(ts)
    mv = np.zeros((n, 6), dtype=bool)
    for i in range(n):
        m = np.abs(ts - ts[i]) <= win
        mv[i] = (q[m].max(axis=0) - q[m].min(axis=0)) > eps
    return mv


def analyze_segments(segs):
    """区間ごとの値 (関節ごと. 止まっていない関節は NaN).
    戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
    out = []
    nan6 = np.full(6, np.nan)
    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
    carry_cmd, carry_min = nan6.copy(), nan6.copy()
    for k, s in enumerate(segs):
        n = len(s["ts"])
        rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
               "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
               "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
        rec["away_max"] = np.zeros(6)
        rec["skip"] = s.get("skip")
        rec["fast"] = bool(s.get("fast"))
        if n and not np.isnan(s["cmd"]).any():
            err = s["q"] - s["cmd"]
            mv = moving_mask(s["ts"], s["q"])
            if s.get("fast"):
                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
                a = np.abs(err)
                same = np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0) & ~np.isnan(carry_min)
                start = np.where(same, np.fmin(carry_min, a[0]), a[0])
                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
                rec["away_max"] = np.max(a - run_min, axis=0)
                carry_cmd, carry_min = s["cmd"].copy(), run_min[-1].copy()
                if s.get("skip") is not None:
                    carry_min[int(s["skip"])] = np.nan
                lag_mv = np.zeros_like(mv)
            else:
                lag_mv = mv
                carry_cmd, carry_min = nan6.copy(), nan6.copy()
            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
            for i in range(6):
                if s.get("skip") is not None and i == int(s["skip"]):
                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                idx = np.flatnonzero(~mv[:, i])
                if s.get("fast") and mv[:, i].any():
                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                if not len(idx):
                    continue
                last = idx[-1]
                first = last
                while first - 1 >= 0 and not mv[first - 1, i]:   # 最後の止まりが続いたところの頭
                    first -= 1
                rec["hold_err"][i] = err[last, i]
                rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
            if s.get("skip") is not None:
                rec["lag_max"][int(s["skip"])] = 0.0
                rec["away_max"][int(s["skip"])] = 0.0
            ce = np.abs(err)
            if s.get("skip") is not None:
                ce[:, int(s["skip"])] = 0.0
            rec["coarse_max"] = ce.max(axis=0)
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        elif n:
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        out.append(rec)
    return out


def _has(v):
    return v is not None and not np.isnan(v)


def log_hold_errors(log):
    """log.jsonl だけで見る止まったあとのずれ. 戻り値: {"correction": [(t, err6)], "chunk": [(t, err6)]}"""
    out = {"correction": [], "chunk": []}
    last_cmd = None
    for r in log:
        if r.get("type") is None and "chunk" in r:
            ms = r.get("measured_state")
            if last_cmd is not None and ms is not None and r.get("state_source") is not None:
                out["chunk"].append((float(r["t"]), np.asarray(ms[:6], float) - last_cmd))
            if r.get("executed") and r.get("sent_targets"):
                last_cmd = np.asarray(r["sent_targets"][-1][:6], float)
        elif r.get("type") == "correction" and r.get("action") == "move" and r.get("sent"):
            if r.get("measured_after") is not None:
                out["correction"].append((float(r["t"]), np.asarray(r["measured_after"][:6], float)
                                          - np.asarray(r["after"][:6], float)))
            last_cmd = np.asarray(r["after"][:6], float)
        elif r.get("type") == "rewind":
            last_cmd = None
    return out


def _stats(vals):
    a = np.abs(np.asarray(vals, float))
    if not len(a):
        return None
    return {"n": int(len(a)), "median": round(float(np.median(a)), 2),
            "p90": round(float(np.percentile(a, 90)), 2), "max": round(float(a.max()), 2)}


def summarize_run(run_dir):
    run_dir = Path(run_dir)
    stem = run_dir.name
    trial = {}
    try:
        trial = json.loads((run_dir / f"{stem}.json").read_text())
    except Exception:
        pass
    log = []
    lp = run_dir / "log.jsonl"
    if lp.exists():
        for line in lp.read_text().splitlines():
            try:
                log.append(json.loads(line))
            except Exception:
                pass
    res = {"run": stem, "outcome": trial.get("outcome"), "corrections": trial.get("corrections")}
    rows = load_rows(run_dir / "recording" / "states.csv")
    segs = analyze_segments(segments(rows, log))
    res["has_send_rows"] = bool(segs)
    lh = log_hold_errors(log)
    res["log"] = {k: {j: _stats([e[i] for _, e in v]) for i, j in enumerate(JOINTS)} for k, v in lh.items()}
    if not segs:
        return res, segs
    work = [s for s in segs if s["stage"] != "home"]   # HOME は学習に入らない. 別に出す
    res["segments"] = len(segs)
    res["work_segments"] = len(work)
    res["coarse_max"] = {j: round(float(max(s.get("coarse_max", np.zeros(6))[i] for s in segs)), 2)
                         for i, j in enumerate(JOINTS)}
    # codex R1 必須: HOME の区間しかない記録でも止まらない (学習に入る区間がなければ, その値は 0 / None)
    res["lag_max"] = {j: round(float(max([s["lag_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}

    def holds(i, ss=work):
        return [float(s["hold_err"][i]) for s in ss if _has(s["hold_err"][i])]
    res["hold"] = {j: _stats(holds(i)) for i, j in enumerate(JOINTS)}
    res["hold_signed_median"] = ({j: round(float(np.median(holds(i))), 2) if holds(i) else None
                                  for i, j in enumerate(JOINTS)} if work else None)
    by = {}
    for s in work:
        key = "grip_after" if s["kind"] == "grip_after" else s["stage"]
        by.setdefault(key, []).append(s)

    def _mx(v):
        return round(float(max(np.abs(v))), 2) if v else None
    res["hold_by_context"] = {k: {"n": len(v), "j4_max": _mx(holds(4, v)),
                                  "j4_median": round(float(np.median(np.abs(holds(4, v)))), 2) if holds(4, v) else None,
                                  "j1_max": _mx(holds(1, v)), "j2_max": _mx(holds(2, v))}
                              for k, v in sorted(by.items())}
    # 学習に入る段 (HOME 以外) で, 止まったあとのずれ・動いている途中の遅れのどちらかが EVENT_DEG を超えた区間・関節の組
    ev = []
    for s in work:
        for i, j in enumerate(JOINTS):
            he = float(s["hold_err"][i]) if _has(s["hold_err"][i]) else None
            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG or s["away_max"][i] > EVENT_DEG:
                e = {"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
                     "joint": j, "cmd": round(float(s["cmd"][i]), 2),
                     "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
                     "hold_err": None if he is None else round(he, 2),
                     "lag_max": round(float(s["lag_max"][i]), 2)}
                if s["away_max"][i] > 0:
                    e["away_max"] = round(float(s["away_max"][i]), 2)      # 依頼 97 (送ったらすぐ返った区間だけ)
                ev.append(e)
    res["events"] = ev
    # 依頼 97: 送ったらすぐ返った区間の数・ゲームパッドの区間の数・組にできなかった rpc 行の数
    res["fast_segments"] = sum(1 for s in segs if s.get("fast"))
    res["pad_segments"] = {k: sum(1 for s in segs if s["kind"] == k) for k in ("pad_step", "pad_hold", "pad_stop",
                                                                             "pad_unknown")}
    res["away_max"] = {j: round(float(max([s["away_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}
    res["event_segments"] = len({e["seg"] for e in ev})   # codex R1・R2 推奨: 区間の数 (区間の番号で数える. 関節は重ねない)
    res["drift_max"] = {j: round(float(max([abs(s["drift"][i]) for s in work
                                            if _has(s["drift"][i]) and s["still_sec"][i] >= LONG_HOLD_SEC] or [0.0])), 2)
                        for i, j in enumerate(JOINTS)}
    # 爪の開け閉めの前後: 爪の rpc の直前の区間の最後の実測 → 爪のあとの区間の最初の実測
    grips = []
    for a, b in zip(segs, segs[1:]):
        if b["kind"] == "grip_after" and a["n"] and b["n"]:
            grips.append({"t": round(b["t0"], 2), "cmd": b["grip_cmd"], "stage": b["stage"],
                          "delta": [round(float(v), 2) for v in b["first_q"] - a["last_q"]],
                          # codex R2 必須: 爪のあとに止まらなかった関節は None (最初の実測のずれで埋めない. それは別に出す)
                          "err_after": [round(float(h), 2) if _has(h) else None for h in b["hold_err"]],
                          "err_first": [round(float(v), 2) for v in b["first_q"] - b["cmd"]]})
    res["grip"] = grips
    homes = [s for s in segs if s["stage"] == "home" and s["n"]]
    res["home"] = [{"t": round(s["t"], 2), "err_last": [round(float(v), 2) for v in s["last_q"] - s["cmd"]],
                    "lag_max": [round(float(v), 2) for v in s["lag_max"]],
                    "drift": [round(float(v), 2) if _has(v) else None for v in s["drift"]],
                    "still_sec": [round(float(v), 1) for v in s["still_sec"]]} for s in homes]
    return res, segs


def timeline(segs, t_start, joint=4, min_abs=0.0):
    """区間ごとの (経過秒, 段, 種類, 止まったあとのずれ, 動いている途中の遅れ, 止まっている間の変化) の一覧."""
    out = []
    for s in segs:
        he = round(float(s["hold_err"][joint]), 2) if _has(s["hold_err"][joint]) else None
        if (he is None or abs(he) < min_abs) and s["lag_max"][joint] < min_abs:
            continue
        out.append({"sec": round(s["t"] - t_start, 1), "stage": s["stage"], "kind": s["kind"],
                    "cmd": round(float(s["cmd"][joint]), 2), "hold_err": he,
                    "lag_max": round(float(s["lag_max"][joint]), 2),
                    "drift": round(float(s["drift"][joint]), 2) if _has(s["drift"][joint]) else None,
                    "still_sec": round(float(s["still_sec"][joint]), 1)})
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--runs-root", default=str(RUNS_ROOT))
    ap.add_argument("--run", action="append", required=True)
    ap.add_argument("--timeline", action="append", default=[], help="区間ごとの一覧を出す run (j4 と j1・j2)")
    ap.add_argument("--json", help="結果を JSON で書く (runs/ の外. 既存のファイルには書かない)")
    args = ap.parse_args()
    root = Path(args.runs_root)
    if str(root).startswith("/Volumes/KIOXIA") and not Path("/Volumes/KIOXIA").is_mount():
        sys.exit("[ABORT] KIOXIA not mounted: /Volumes/KIOXIA")
    out = {"params": {"move_win_sec": MOVE_WIN_SEC, "move_eps_deg": MOVE_EPS_DEG, "event_deg": EVENT_DEG,
                      "long_hold_sec": LONG_HOLD_SEC, "fast_rpc_sec": FAST_RPC_SEC}, "runs": {}, "timelines": {}}
    for stem in args.run:
        res, segs = summarize_run(root / stem)
        out["runs"][stem] = res
        print(f"## {stem}  outcome={res['outcome']} corrections={res['corrections']}")
        if not res["has_send_rows"]:
            print("   send_angles 行なし (依頼 46 より前). log で見る値だけ")
        else:
            print("   粗い最大   " + " ".join(f"{j}={v:5.1f}" for j, v in res["coarse_max"].items()))
            print("   動き途中   " + " ".join(f"{j}={v:5.1f}" for j, v in res["lag_max"].items()))
            def _h(k):          # codex R2 推奨: 止まった区間のない関節は「-」(0.0 と取り違えない)
                return " ".join(f"{j}={'-' if res['hold'][j] is None else format(res['hold'][j][k], '5.1f'):>5}"
                                for j in JOINTS)
            print("   止まり最大 " + _h("max"))
            print("   止まり90%  " + _h("p90"))
            print("   止まり中央 " + _h("median"))
            print("   止まり符号 " + " ".join(f"{j}={'-' if v is None else f'{v:+.1f}':>5}" for j, v in (res["hold_signed_median"] or {}).items()))
            print("   じわじわ   " + " ".join(f"{j}={v:5.1f}" for j, v in res["drift_max"].items()))
            if res["fast_segments"]:
                print("   向かう途中に離れた " + " ".join(f"{j}={v:5.1f}" for j, v in res["away_max"].items())
                      + f"  (すぐ返った区間 {res['fast_segments']}・ゲームパッド {res['pad_segments']})")
            print(f"   段ごと {res['hold_by_context']}")
            print(f"   外れ (>{EVENT_DEG:.0f}°) {res['event_segments']} 区間 ({len(res['events'])} 区間・関節の組)")
            for e in res["events"]:
                print(f"     {e['sec']:6.1f}秒 {e['stage']:7s} {e['kind']:10s} {e['joint']} 指令 {e['cmd']:7.2f} "
                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}"
                      + (f"  離れた {e['away_max']}" if "away_max" in e else ""))
            for gr in res["grip"]:
                print(f"   爪 {gr['cmd']} ({gr['stage']}) 前後の変化 {gr['delta']}  爪のあとのずれ {gr['err_after']}")
            for h in res["home"]:
                print(f"   HOME 最後のずれ {h['err_last']}  動き途中 {h['lag_max']}  止まり中の変化 {h['drift']}")
        for k in ("correction", "chunk"):
            st = res["log"][k]
            if st["j4"]:
                print(f"   log {k:10s} n={st['j4']['n']:2d} " + " ".join(
                    f"{j}={st[j]['max']:5.1f}" for j in JOINTS) + "  (最大)")
        if stem in args.timeline and segs:
            t0 = segs[0]["t"]
            tl = {j: timeline(segs, t0, joint=i) for i, j in ((1, "j1"), (2, "j2"), (4, "j4"))}
            out["timelines"][stem] = tl
            print("   区間ごと (j4): 秒 段 種類 指令 止まりのずれ 動き途中 止まり中の変化 止まった秒")
            for e in tl["j4"]:
                print(f"     {e['sec']:6.1f} {e['stage']:7s} {e['kind']:10s} {e['cmd']:7.2f} "
                      f"{'' if e['hold_err'] is None else e['hold_err']:>7} {e['lag_max']:6.2f} "
                      f"{'' if e['drift'] is None else e['drift']:>6} {e['still_sec']:5.1f}")
    if args.json:
        p = Path(args.json)
        if p.exists():
            sys.exit(f"[ABORT] 既にある: {p}")
        # 止まらなかった関節 (NaN) は null にする (JSON に NaN を書かない)
        p.write_text(json.dumps(out, indent=1, ensure_ascii=False, allow_nan=False,
                                default=lambda o: [None if isinstance(x, float) and np.isnan(x) else x for x in o.tolist()]))
        print(f"[JSON] {p}")


if __name__ == "__main__":
    main()

exec
/bin/zsh -lc 'cat pi05/test_diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
exec
/bin/zsh -lc "head -65 docs/diag_joint_hold_20261106.txt; rg -n 'resends|t_send|t_halt' pi05/gamepad_teleop.py" in /Users/shimazueiichi/r2r2r-scripts
exec
/bin/zsh -lc 'cat docs/review_result_20261106_corr_v5_gamepad_codex.md' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 330ms:
# codex レビュー結果：ゲームパッドの点検・corr_v5・corr3（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad.md（codex exec -s read-only）

**corr3 の学習・推論の向け先は整合しています。ただし、診断コードには必須修正が 2 点あります。** 今回の「4 本を採用・185554 を除外」という結果は再現しましたが、「手首の垂れを見落とさない」とはまだ断定できません。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| pad 送信の対応付け | **必須** | [_pad_send_for](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:127) は `ts + 0.2` 以前の最新送信を選ぶため、次の送信を拾えます。送信 A=1.00 秒、B=1.15 秒、rpc=1.00 秒で **B を選ぶ**ことを再現しました。初回は時刻差が最小の未使用送信に対応させ、再送は対応済み送信の残り回数として扱い、曖昧なら unknown にしてください。今回の 5 本では未来送信との対応はありませんでした。 |
| fast の区間をまたぐ垂れ | **必須** | [away の計算](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:222) が区間ごとにリセットされます。j4 の指令が同じでも、別関節の送信で区切られ、実測が `0→3°`、次区間で `3→6°` と動き続けると、両区間とも away=3°・hold_err=NaN となり、6° の垂れを見逃します。**関節ごとに、指令が変わらない間の最小誤差を引き継ぐ**必要があります。再送・爪操作でも同様です。 |
| 再送の数え方 | 据え置き | `n_used > resends` の条件は初回 1 回＋再送数を許すため、回数に off-by-one はありません。ただし上記の対応付けを誤ると回数管理も崩れます。 |
| stop の指令復元 | 据え置き／推奨 | 既知の halt_joint だけ measured_after_halt に替える処理は `_apply_halt` と一致します。一方、halt の検索は範囲内の最初の行を選び、消費済み管理がありません。最近傍・一対一対応にすることを推奨します。 |
| fast の通常動作の扱い | 据え置き | 指令へ単調に近づく動きを lag として数えない方針は妥当です。動き出す前の止まりを除く処理も意図と一致します。ただし、away は原因を区別しないため、4°超のオーバーシュートも「外れ」になります。 |
| 11-01 との互換性 | 据え置き：確認済み | 9 本すべて fast=0。既存 JSON にある全結果項目が一致しました。 |
| 11-06 の選別結果 | 据え置き：再現済み | 5 本とも pad_unknown=0。採用する 4 本は events なし。185554 は j4 の外れが 4 区間で、最大約 6.8°。ただし上記の検出漏れを直した後の再点検が必要です。 |
| corr3 の学習経路 | 据え置き：整合 | [モデル定義](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_lib.sh:306)から、v5 tar → Pod の v5 データ → `pi05_mecharm_trim_corr_v5_lora`／`local/..._trim2_corr_v5` へ一致。初期重みは corr2 を `/workspace/ckpt/corr_v2_1499` に取得し、その params を使用します。 |
| 出力・由来 | 据え置き：整合 | HF 出力は `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3`、KIOXIA は `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499`。由来の入力も corr2 の `..._v2/1499`、データも v5 です。既存 trim・corr・corr2 の分岐に動作変更は見つかりませんでした。 |
| 正規化と推論 assets | 据え置き：整合 | corr2 の v4b assets にある正規化ファイルの SHA256 は指定値と一致。v5 の repo_id が既定 asset_id となるため、保存先は `assets/local/..._trim2_corr_v5` で、corr3 推論のコピー元と一致します。[固定コミットの設定実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/config.py)、[保存実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/checkpoints.py) |
| Log22 の Pod 記録 | 据え置き：分離済み | 推論は `Log22/pod/lab_pod.json`、学習は `Log22/train/lab_pod.json` で衝突しません。推論側には 18:58:28 の終了記録があります。確認時点では train ディレクトリは未作成でした。 |
| データの構成 | 据え置き：確認済み | 統合版は 30 本・4301 コマ。旧 7 本の episode.npz はバイト一致。tar 内も 30 本で `._` はありません。 |
| 184650 の範囲超過 | **推奨：評価条件に明記** | 実データの j2 上限は trim2=31.37°、v5=42.45°。推論側は trim2＋10°なので上限約41.37°となり、**学習軌道の末端約1.08°はクランプされます**。追加自体を禁止する理由にはなりませんが、同じ軌道を完全再現できない条件として扱ってください。 |
| 4.8°／6.7° のコマ間移動 | 推奨 | 従来より大きい動きを教師にするため、出力の移動量や補正頻度が増える可能性があります。正規化の維持は妥当ですが、成功率だけでなくクランプ・移動量制限の発生数も比較すると判断できます。1コマ差に加え、学習が使うチャンク先頭からの差分も確認すると有用です。 |
| 学習テストの assets | 推奨 | [偽物チェックポイント](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_train_fakes.sh:72)は v3 assets 固定です。corr3 では v5 に切り替え、取得した assets 名まで検査すると学習→推論間の不整合を検出できます。 |

必須は **送信の対応付け**と**指令不変の関節で away を区間越しに保持する処理**です。この修正後に 5 本を再点検してから採用を確定するのが適切です。

読み取り専用の実ログテスト 2 件は成功しました。一時ファイルを生成するテスト群は未実行です。ファイル変更、実機・Pod・Hugging Face の操作は行っていません。

 succeeded in 433ms:
#!/usr/bin/env python3
"""test_diag_joint_hold.py — diag_joint_hold.py (依頼 57) の確かめ.

  - 動いている途中の遅れと, 止まったあとのずれを分ける (rpc が返ったあとの実測だけを使う)
  - 指令が変わらないのに動いた区間を「外れ」として数える. HOME は数えない
  - 爪の rpc で区間を切る
  - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
  - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
"""
import csv
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

import numpy as np

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))

import diag_joint_hold as d  # noqa: E402

COLS = ["ts", "event_type", "stage", "j0", "j1", "j2", "j3", "j4", "j5", "speed", "gripper_state",
        "ts_send", "ts_arrival", "attempt", "arrived", "note"]


class Rec:
    def __init__(self):
        self.rows = []

    def send(self, t, q, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})

    def rpc(self, t, cmd, stage="correct", rpc=0.03):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})

    def grip(self, t, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})

    def actual(self, t0, t1, q, stage="model", dt=0.1):
        t = t0
        while t < t1 - 1e-9:
            qq = q(t) if callable(q) else q
            self.rows.append({"ts": round(t, 3), "event_type": "actual", "stage": stage, **_j(qq)})
            t += dt

    def write(self, run_dir, log=()):
        rec = run_dir / "recording"
        rec.mkdir(parents=True)
        with (rec / "states.csv").open("w", newline="") as f:
            w = csv.DictWriter(f, fieldnames=COLS)
            w.writeheader()
            for r in self.rows:
                w.writerow({k: r.get(k, "") for k in COLS})
        (run_dir / "log.jsonl").write_text("".join(json.dumps(x) + "\n" for x in log))
        (run_dir / f"{run_dir.name}.json").write_text(json.dumps({"outcome": "success", "corrections": 1}))


def _j(q):
    return {f"j{i}": f"{v:.3f}" for i, v in enumerate(q)}


def pose(j4, j1=10.0):
    return [0.0, j1, 0.0, 0.0, j4, -30.0]


class TestSegments(unittest.TestCase):
    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_x"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_hold_vs_lag_and_events(self):
        r = Rec()
        # 1. 送って rpc が返ったあと, 指令から 1° ずれて止まる (ふつうの止まり)
        r.send(0.0, pose(-50))
        r.actual(1.5, 4.0, pose(-49))
        # 2. 送ったあと 2 秒かけて -60 → -55 へ動き (指令は -60), そのまま止まる (外れ: 指令なしに +5°)
        r.send(4.0, pose(-60))
        r.actual(5.5, 7.5, lambda t: pose(-60 + 5 * min(1.0, (t - 5.5) / 1.0)))
        r.actual(7.5, 9.0, pose(-55))
        # 3. HOME で戻らない (数えない)
        r.send(9.0, pose(0), stage="home")
        r.actual(10.5, 12.0, pose(40), stage="home")
        res, segs = self.build(r)
        self.assertEqual(len(segs), 3)
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 5.0, places=1)       # 止まったあとのずれ (2.)
        self.assertAlmostEqual(res["lag_max"]["j4"], 5.0, places=1)           # 動いている途中 (2. の頭は 0° ずれ… 最後の動きで 5°)
        self.assertEqual(res["hold_by_context"]["model"]["n"], 2)
        ev = res["events"]
        self.assertEqual([(e["sec"], e["joint"]) for e in ev], [(4.0, "j4")])  # HOME は数えない
        self.assertAlmostEqual(ev[0]["hold_err"], 5.0, places=1)
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 40.0, places=1)
        self.assertAlmostEqual(res["coarse_max"]["j4"], 40.0, places=1)       # 粗い値は HOME も含む
        self.assertAlmostEqual(res["hold_signed_median"]["j1"], 0.0, places=2)

    def test_no_samples_during_rpc_and_still_threshold(self):
        r = Rec()
        r.send(0.0, pose(-50), rpc=1.6)
        r.actual(0.0, 1.5, pose(-30))            # rpc の途中の値 (使わない)
        r.actual(1.6, 3.0, pose(-50.2))
        res, segs = self.build(r)
        self.assertEqual(segs[0]["n"], 14)
        self.assertEqual(res["events"], [])
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 0.2, places=2)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_grip_splits_segment(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 2.5, pose(-50))
        r.grip(2.5)
        r.actual(4.0, 5.0, pose(-46))             # 爪のあとで 4° 動いていた (外れの手前)
        res, segs = self.build(r)
        self.assertEqual([s["kind"] for s in segs], ["send", "grip_after"])
        self.assertEqual(res["grip"][0]["delta"][4], 4.0)
        self.assertAlmostEqual(res["hold_by_context"]["grip_after"]["j4_max"], 4.0, places=2)
        self.assertEqual(res["events"], [])        # 4.0 は外れの目安 (4° を超える) に入らない

    def test_drift_while_still(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 6.5, lambda t: pose(-50 + 0.2 * (t - 1.5)))   # 0.2°/秒 でじわじわ (動いているとはしない)
        res, _ = self.build(r)
        self.assertAlmostEqual(res["drift_max"]["j4"], 1.0, delta=0.05)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_joint_wise_still(self):
        """codex R1 必須: j1 が動き続けていても, 止まった j4 の 5° のずれを見落とさない."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))   # j1 は 2°/秒 で動き続ける. j4 は -55 で止まり
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][1]))                    # j1 は一度も止まっていない
        self.assertAlmostEqual(segs[0]["hold_err"][4], 5.0, places=2)
        self.assertEqual([e["joint"] for e in res["events"]], ["j1", "j4"])
        self.assertEqual(res["event_segments"], 1)                           # 区間の数は 1
        self.assertIsNone(res["hold"]["j1"])

    def test_grip_after_not_still_is_none(self):
        """codex R2 必須: 爪のあとに止まらなかった関節のずれは None (最初の実測のずれで埋めない)."""
        r = Rec()
        r.send(0.0, pose(-50, j1=10))
        r.actual(1.5, 2.5, pose(-50, j1=10))
        r.grip(2.5)
        r.actual(4.0, 6.0, lambda t: pose(-50, j1=10 + 2.0 * (t - 4.0)))
        res, _ = self.build(r)
        g = res["grip"][0]
        self.assertIsNone(g["err_after"][1])
        self.assertEqual(g["err_after"][4], 0.0)
        self.assertEqual(g["err_first"][1], 0.0)

    def test_event_segments_by_index(self):
        """codex R2 推奨: 秒が近い別の区間も別に数える."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.01)
        r.actual(0.01, 0.03, pose(-50))
        r.send(0.04, pose(-60), rpc=0.01)
        r.actual(0.05, 1.0, pose(-50))
        res, _ = self.build(r)
        self.assertEqual(res["event_segments"], 2)

    def test_home_only(self):
        """codex R1 必須: HOME の区間しかない記録でも止まらない."""
        r = Rec()
        r.send(0.0, pose(0), stage="home")
        r.actual(1.5, 3.0, pose(1), stage="home")
        res, _ = self.build(r)
        self.assertEqual(res["work_segments"], 0)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertIsNone(res["hold"]["j4"])
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 1.0, places=2)

    def test_drift_uses_last_still_run(self):
        """codex R1 推奨: 止まり中の変化は最後の止まりが続いたところだけ (途中で動いた分を入れない)."""
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 3.0, pose(-50))
        r.actual(3.0, 3.5, lambda t: pose(-50 + 10 * (t - 3.0)))           # 途中で 5° 動く
        r.actual(3.5, 6.0, pose(-45))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["drift"][4], 0.0, places=2)
        self.assertGreater(segs[0]["still_sec"][4], 1.5)
        self.assertLess(segs[0]["still_sec"][4], 2.5)

    def test_json_has_no_nan(self):
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))
        td = Path(tempfile.mkdtemp())
        r.write(td / "pi05_y")
        out = td / "o.json"
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--timeline", "pi05_y",
                                     "--json", str(out)]), mock.patch("sys.stdout"):
            d.main()
        self.assertNotIn("NaN", out.read_text())
        json.loads(out.read_text())
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--json", str(out)]), \
                mock.patch("sys.stdout"), self.assertRaises(SystemExit):
            d.main()                                                          # 既にあるファイルには書かない

    def test_log_hold_errors(self):
        log = [
            {"chunk": 0, "t": 1.0, "executed": True, "state_source": "measured",
             "measured_state": pose(-1) + [0.0], "sent_targets": [pose(-40)]},
            {"chunk": 1, "t": 2.0, "executed": False, "state_source": "target",
             "measured_state": pose(-37) + [0.0], "sent_targets": [pose(-45)]},
            {"type": "correction", "action": "move", "sent": True, "t": 3.0,
             "after": pose(-45), "measured_after": pose(-30)},
            {"type": "rewind", "t": 4.0},
            {"chunk": 2, "t": 5.0, "executed": True, "state_source": "measured", "measured_state": pose(9) + [0.0]},
        ]
        out = d.log_hold_errors(log)
        self.assertEqual([round(float(e[4]), 2) for _, e in out["chunk"]], [3.0])     # 実行していないチャンクの目標は使わない. 巻き戻しのあとは比べない
        self.assertEqual([round(float(e[4]), 2) for _, e in out["correction"]], [15.0])

    def test_old_record_without_send_rows(self):
        r = Rec()
        r.actual(0.0, 2.0, pose(-50))
        res, segs = self.build(r)
        self.assertFalse(res["has_send_rows"])
        self.assertEqual(segs, [])


def pad_move(t, command, joint, hold=False, resends=(), cid=1):
    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
         "joint": joint, "resends": list(resends)}
    if hold:
        r["no_check"] = True
    return r


def pad_halt(t, q, joint, cid=1):
    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
            "halt_joint": joint, "halt": "stop"}


class TestPadAndFast(unittest.TestCase):
    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""

    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_p"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_fast_approach_is_not_lag(self):
        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
        r.actual(2.3, 3.5, pose(-59.5))
        res, segs = self.build(r)
        self.assertEqual(res["fast_segments"], 1)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
        self.assertLess(res["away_max"]["j4"], 0.2)

    def test_fast_hold_ignores_still_before_moving(self):
        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.8, pose(-50))
        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
        self.assertEqual(res["events"], [])

    def test_fast_sag_after_arrival_is_event(self):
        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 1.0, pose(-60))
        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
        r.actual(1.6, 3.0, pose(-54))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)

    def test_pad_step_hold_stop(self):
        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
        start = pose(-60, j1=40)
        step = pose(-60, j1=40); step[0] = 1.0
        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
        r = Rec()
        r.send(0.0, start, stage="model", rpc=0.03)
        r.actual(0.03, 1.0, start, stage="model")
        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
        r.actual(2.03, 3.0, step, stage="correct")
        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)
        r.actual(3.03, 3.5, lambda t: [1.0, 40 - 20 * (t - 3.03), 0, 0, -60, -30], stage="correct")
        r.rpc(3.5, "stop")
        r.actual(3.55, 5.0, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
        r.rpc(5.0, "send_angle")                                               # 組にできない (pad の行がない… 送り直しの数を越える)
        r.actual(5.03, 5.5, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
        log = [pad_move(2.0, step, 0), pad_move(3.0, far, 1, hold=True), pad_halt(3.5, [1.0, 30.6, 0, 0, -60.2, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_step", "pad_hold", "pad_stop", "pad_unknown"])
        self.assertEqual(res["pad_segments"], {"pad_step": 1, "pad_hold": 1, "pad_stop": 1, "pad_unknown": 1})
        np.testing.assert_allclose(segs[1]["cmd"], step)
        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
        self.assertEqual(segs[2]["lag_max"][1], 0.0)
        self.assertEqual(segs[2]["skip"], 1)
        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
        self.assertEqual(res["events"], [])

    def test_pad_resend_and_uncommanded_wrist(self):
        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
        start = pose(-60, j1=50)
        far = pose(-60, j1=35)
        r = Rec()
        r.send(0.0, start, rpc=0.03)
        r.actual(0.03, 1.0, start)
        r.rpc(2.0, "send_angle")
        r.rpc(2.3, "send_angle")                                               # 送り直し
        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
        r.rpc(3.0, "stop")
        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)

    def test_pad_send_not_matched_to_next(self):
        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "send_angle")
        r.actual(1.03, 1.14, a_cmd, stage="correct")
        r.rpc(1.16, "send_angle")
        r.actual(1.19, 2.0, b_cmd, stage="correct")
        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
        _, segs = self.build(r, log)
        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)

    def test_away_carries_across_fast_segments(self):
        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10), rpc=0.03)
        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
        res, segs = self.build(r)
        self.assertLess(segs[0]["away_max"][4], 3.1)
        self.assertGreater(segs[1]["away_max"][4], 5.5)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        r2 = Rec()                                          # j4 の指令が変われば引き継がない
        r2.send(0.0, pose(-60), rpc=0.03)
        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
        r2.send(1.0, pose(-57), rpc=0.03)
        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
        res2, _ = self.build(r2)
        self.assertEqual(res2["events"], [])


RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")


@unittest.skipUnless(RUNS.is_dir(), "KIOXIA がない")
class TestToday(unittest.TestCase):
    """依頼文の粗い値 (j4 の「直前に送った指令と実測の差」の最大) と同じになり, 入れる 3 本には外れがない."""
    COARSE = {"190347": 3.5, "190854": 2.5, "192101": 1.8, "192418": 10.3, "192758": 19.5,
              "193041": 18.4, "193411": 8.0, "194042": 59.5}

    def test_coarse_and_events(self):
        for stem, v in self.COARSE.items():
            res, _ = d.summarize_run(RUNS / f"pi05_20261101_{stem}")
            self.assertAlmostEqual(res["coarse_max"]["j4"], v, delta=0.051, msg=stem)
            n_ev = len([e for e in res["events"] if e["joint"] == "j4"])
            if stem in ("190347", "190854", "192101"):
                self.assertEqual(n_ev, 0, stem)
            else:
                self.assertGreater(n_ev, 0, stem)

    def test_20261106_gamepad(self):
        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
        for stem in ("183926", "184452", "184650", "184906", "185554"):
            p = RUNS / f"pi05_20261106_{stem}"
            if not p.is_dir():
                self.skipTest("11-06 の記録がない")
            res, _ = d.summarize_run(p)
            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
            self.assertGreater(res["fast_segments"], 0, stem)
            if stem == "185554":
                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
            else:
                self.assertEqual(res["events"], [], stem)


if __name__ == "__main__":
    unittest.main()

 exited 2 in 472ms:
## pi05_20261106_183926  outcome=success corrections=2
   粗い最大   j0=  8.0 j1=  9.1 j2=  5.5 j3=  2.0 j4=  9.6 j5=  1.8
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.7 j1=  2.1 j2=  2.1 j3=  1.1 j4=  2.2 j5=  0.7
   止まり90%  j0=  0.5 j1=  1.9 j2=  1.6 j3=  0.6 j4=  1.8 j5=  0.6
   止まり中央 j0=  0.1 j1=  0.9 j2=  0.9 j3=  0.3 j4=  0.8 j5=  0.3
   止まり符号 j0= +0.0 j1= +0.9 j2= +0.9 j3= +0.1 j4= +0.2 j5= -0.1
   じわじわ   j0=  0.2 j1=  0.2 j2=  0.2 j3=  0.3 j4=  0.9 j5=  0.3
   向かう途中に離れた j0=  0.3 j1=  1.1 j2=  0.8 j3=  0.8 j4=  1.2 j5=  0.2  (すぐ返った区間 72・ゲームパッド {'pad_step': 9, 'pad_hold': 4, 'pad_stop': 4, 'pad_unknown': 0})
   段ごと {'correct': {'n': 17, 'j4_max': 1.67, 'j4_median': 0.18, 'j1_max': 2.0, 'j2_max': 1.3}, 'grip_after': {'n': 2, 'j4_max': 2.21, 'j4_median': 1.34, 'j1_max': 1.07, 'j2_max': 1.43}, 'model': {'n': 51, 'j4_max': 2.03, 'j4_median': 1.03, 'j1_max': 2.13, 'j2_max': 2.15}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.45, 0.54, 0.68, 0.53, -2.21, -0.25]
   爪 set_gripper (model) 前後の変化 [-0.09, 0.0, 0.0, 0.09, 0.0, -0.08]  爪のあとのずれ [0.0, 1.07, 1.43, -0.23, 0.47, -0.26]
   HOME 最後のずれ [-0.14, 1.48, 2.15, 0.3, -0.65, 0.08]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
   HOME 最後のずれ [0.0, 1.14, 1.4, -0.43, -0.43, -0.49]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, 0.0, None, 0.18]
   log chunk      n=16 j0=  2.1 j1= 31.5 j2=  2.1 j3=  0.9 j4= 16.6 j5=  0.7  (最大)
## pi05_20261106_184452  outcome=success corrections=2
   粗い最大   j0= 25.3 j1= 28.1 j2=  8.2 j3=  2.0 j4= 30.7 j5=  1.7
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  1.0 j1=  2.6 j2=  1.9 j3=  1.7 j4=  3.3 j5=  1.2
   止まり90%  j0=  0.8 j1=  1.3 j2=  1.2 j3=  1.1 j4=  2.0 j5=  0.6
   止まり中央 j0=  0.2 j1=  0.9 j2=  0.4 j3=  0.3 j4=  0.7 j5=  0.3
   止まり符号 j0= -0.1 j1= +0.9 j2= +0.4 j3= +0.1 j4= +0.6 j5= -0.1
   じわじわ   j0=  0.3 j1=  0.1 j2=  0.0 j3=  0.3 j4=  0.2 j5=  0.9
   向かう途中に離れた j0=  1.2 j1=  1.8 j2=  1.2 j3=  1.5 j4=  3.8 j5=  0.2  (すぐ返った区間 61・ゲームパッド {'pad_step': 11, 'pad_hold': 5, 'pad_stop': 4, 'pad_unknown': 0})
   段ごと {'correct': {'n': 20, 'j4_max': 2.99, 'j4_median': 0.62, 'j1_max': 1.84, 'j2_max': 1.1}, 'grip_after': {'n': 2, 'j4_max': 2.04, 'j4_median': 1.27, 'j1_max': 1.19, 'j2_max': 1.18}, 'model': {'n': 37, 'j4_max': 3.34, 'j4_median': 0.77, 'j1_max': 2.6, 'j2_max': 1.95}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.6, 0.84, 0.93, 0.41, -2.04, 0.13]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, -1.06, 0.0]  爪のあとのずれ [-0.51, 1.19, 1.18, -0.2, 0.49, 0.27]
   HOME 最後のずれ [0.23, 1.77, 1.02, 0.52, -0.84, -0.45]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.09]
   HOME 最後のずれ [-0.26, 1.4, 1.75, 0.7, -0.79, -1.02]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, -0.53, None, 0.0]
   log chunk      n=12 j0=  2.7 j1= 24.1 j2=  3.0 j3=  0.8 j4=  4.3 j5=  1.3  (最大)
## pi05_20261106_184650  outcome=success corrections=2
   粗い最大   j0= 22.0 j1= 24.3 j2=  7.2 j3=  1.9 j4= 30.5 j5=  1.9
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.8 j1=  2.6 j2=  1.9 j3=  0.9 j4=  3.0 j5=  0.6
   止まり90%  j0=  0.6 j1=  2.0 j2=  1.5 j3=  0.5 j4=  2.5 j5=  0.4
   止まり中央 j0=  0.1 j1=  1.1 j2=  1.0 j3=  0.3 j4=  0.8 j5=  0.2
   止まり符号 j0= -0.1 j1= +0.8 j2= +1.0 j3= -0.1 j4= +0.0 j5= +0.0
   じわじわ   j0=  0.3 j1=  0.1 j2=  0.5 j3=  0.2 j4=  0.3 j5=  0.2
   向かう途中に離れた j0=  0.4 j1=  2.6 j2=  1.3 j3=  1.1 j4=  2.7 j5=  0.2  (すぐ返った区間 59・ゲームパッド {'pad_step': 1, 'pad_hold': 6, 'pad_stop': 3, 'pad_unknown': 0})
   段ごと {'correct': {'n': 10, 'j4_max': 2.99, 'j4_median': 0.27, 'j1_max': 2.63, 'j2_max': 1.85}, 'grip_after': {'n': 3, 'j4_max': 2.84, 'j4_median': 2.11, 'j1_max': 1.49, 'j2_max': 1.29}, 'model': {'n': 43, 'j4_max': 2.8, 'j4_median': 0.75, 'j1_max': 2.16, 'j2_max': 1.82}, 'wait': {'n': 1, 'j4_max': 2.11, 'j4_median': 2.11, 'j1_max': 1.49, 'j2_max': 0.44}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.18, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.7, 0.62, 1.07, 0.38, -2.84, -0.31]
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [0.08, -1.49, 0.44, -0.26, -2.11, 0.0]
   爪 set_gripper (model) 前後の変化 [0.35, 0.09, -0.18, -0.27, 0.0, 0.0]  爪のあとのずれ [0.12, 1.21, 1.29, -0.06, 0.72, 0.33]
   HOME 最後のずれ [0.01, 1.79, 1.08, 0.66, -0.82, -0.57]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.18, 0.0]
   HOME 最後のずれ [-0.17, 1.49, 1.75, 0.52, -0.87, -0.93]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, None, None, None]
   log chunk      n=14 j0=  1.0 j1= 42.1 j2= 17.1 j3=  0.7 j4=  2.8 j5=  0.6  (最大)
## pi05_20261106_184906  outcome=success corrections=2
   粗い最大   j0= 39.7 j1= 47.5 j2=  7.1 j3=  2.5 j4= 55.9 j5=  1.7
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  1.0 j1=  3.0 j2=  2.8 j3=  0.9 j4=  3.1 j5=  1.4
   止まり90%  j0=  0.5 j1=  1.5 j2=  1.7 j3=  0.6 j4=  2.7 j5=  0.6
   止まり中央 j0=  0.1 j1=  1.0 j2=  0.8 j3=  0.3 j4=  1.4 j5=  0.2
   止まり符号 j0= -0.1 j1= +1.0 j2= +0.8 j3= +0.3 j4= -1.4 j5= +0.0
   じわじわ   j0=  0.3 j1=  0.2 j2=  0.3 j3=  0.4 j4=  1.4 j5=  1.0
   向かう途中に離れた j0=  0.3 j1=  1.0 j2=  0.8 j3=  0.9 j4=  2.4 j5=  0.0  (すぐ返った区間 53・ゲームパッド {'pad_step': 5, 'pad_hold': 3, 'pad_stop': 2, 'pad_unknown': 0})
   段ごと {'correct': {'n': 10, 'j4_max': 2.72, 'j4_median': 2.72, 'j1_max': 1.15, 'j2_max': 1.64}, 'grip_after': {'n': 3, 'j4_max': 3.15, 'j4_median': 1.59, 'j1_max': 1.13, 'j2_max': 1.54}, 'model': {'n': 37, 'j4_max': 2.67, 'j4_median': 0.87, 'j1_max': 2.99, 'j2_max': 2.83}, 'wait': {'n': 1, 'j4_max': 1.49, 'j4_median': 1.49, 'j1_max': 0.0, 'j2_max': 0.79}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.49, -0.06, 1.54, 0.8, -3.15, -0.02]
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [0.0, 0.0, 0.79, 0.27, -1.49, -0.18]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, -0.09]  爪のあとのずれ [-0.12, 1.13, 1.41, -0.2, -1.59, 0.29]
   HOME 最後のずれ [0.28, 2.04, 1.37, -0.18, -1.3, 0.3]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, -0.08, 0.0, 0.0, 0.35, 0.0]
   HOME 最後のずれ [0.0, 1.49, 1.75, 1.05, -1.05, -0.58]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, None, None, 0.09]
rg: pi05/gamepad_teleop.py: IO error for operation on pi05/gamepad_teleop.py: No such file or directory (os error 2)

exec
/bin/zsh -lc "rg --files -g '*gamepad*' -g '*diag_joint_hold*' -g 'AGENTS.md'; nl -ba pi05/diag_joint_hold.py | sed -n '218,282p'; head -75 pi05/diag_joint_hold.py" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 158ms:
AGENTS.md
pi05/test_diag_joint_hold.py
pi05/diag_joint_hold.py
tools/gamepad_check.py
tools/test_gamepad_teleop.py
tools/gamepad_teleop.py
docs/diag_joint_hold_20261101.json
docs/review_result_20261103_gamepad_hold.md
docs/diag_joint_hold_20261106.txt
docs/diag_joint_hold_20261101.txt
docs/review_request_20261106_corr_v5_gamepad.md
docs/diag_joint_hold_20261106.json
docs/review_result_20261106_corr_v5_gamepad_codex.md
docs/review_request_20261106_corr_v5_gamepad_r2.md
docs/review_request_20261103_gamepad_hold.md
   218	    """区間ごとの値 (関節ごと. 止まっていない関節は NaN).
   219	    戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
   220	    out = []
   221	    nan6 = np.full(6, np.nan)
   222	    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
   223	    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
   224	    carry_cmd, carry_min = nan6.copy(), nan6.copy()
   225	    for k, s in enumerate(segs):
   226	        n = len(s["ts"])
   227	        rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
   228	               "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
   229	               "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
   230	        rec["away_max"] = np.zeros(6)
   231	        rec["skip"] = s.get("skip")
   232	        rec["fast"] = bool(s.get("fast"))
   233	        if n and not np.isnan(s["cmd"]).any():
   234	            err = s["q"] - s["cmd"]
   235	            mv = moving_mask(s["ts"], s["q"])
   236	            if s.get("fast"):
   237	                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
   238	                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
   239	                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
   240	                a = np.abs(err)
   241	                same = np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0) & ~np.isnan(carry_min)
   242	                start = np.where(same, np.fmin(carry_min, a[0]), a[0])
   243	                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
   244	                rec["away_max"] = np.max(a - run_min, axis=0)
   245	                carry_cmd, carry_min = s["cmd"].copy(), run_min[-1].copy()
   246	                if s.get("skip") is not None:
   247	                    carry_min[int(s["skip"])] = np.nan
   248	                lag_mv = np.zeros_like(mv)
   249	            else:
   250	                lag_mv = mv
   251	                carry_cmd, carry_min = nan6.copy(), nan6.copy()
   252	            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
   253	            for i in range(6):
   254	                if s.get("skip") is not None and i == int(s["skip"]):
   255	                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
   256	                idx = np.flatnonzero(~mv[:, i])
   257	                if s.get("fast") and mv[:, i].any():
   258	                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
   259	                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
   260	                if not len(idx):
   261	                    continue
   262	                last = idx[-1]
   263	                first = last
   264	                while first - 1 >= 0 and not mv[first - 1, i]:   # 最後の止まりが続いたところの頭
   265	                    first -= 1
   266	                rec["hold_err"][i] = err[last, i]
   267	                rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
   268	                rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
   269	            if s.get("skip") is not None:
   270	                rec["lag_max"][int(s["skip"])] = 0.0
   271	                rec["away_max"][int(s["skip"])] = 0.0
   272	            ce = np.abs(err)
   273	            if s.get("skip") is not None:
   274	                ce[:, int(s["skip"])] = 0.0
   275	            rec["coarse_max"] = ce.max(axis=0)
   276	            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
   277	        elif n:
   278	            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
   279	        out.append(rec)
   280	    return out
   281	
   282	
#!/usr/bin/env python3
"""diag_joint_hold.py — 実機推論の記録から, 関節ごとの「動いている途中の遅れ」と「止まったあとのずれ (垂れ)」を出す (2026-11-01 依頼 57).

読むだけ (runs/ には書かない). アームは使わない.

入力: runs/pi05_policy/{run_stem}/recording/states.csv と log.jsonl
  states.csv (依頼 46 のあとの記録) には send_angles 行 (送った時刻・目標・rpc_sec) と, 爪の rpc 行がある.
  依頼 46 より前の記録 (send_angles 行がない) は log.jsonl だけで見る (下の「log で見る」).

分け方 (states.csv で見る):
  - 指令 = 直前の send_angles の目標. 送ってから rpc が返るまで (daemon が着いたと判定するまで. 約 1.6 秒) は
    つながりがふさがり, 実測は残らない. rpc が返ったあとから次の送信までを「止まっているはずの区間」(hold) とする.
    爪の rpc (開け閉め. 指令は変わらない) でも区間を切る (爪の前・爪の後 を別に見る).
  - 区間の中のコマごと・関節ごとに, 前後 MOVE_WIN_SEC 以内の実測のふれが MOVE_EPS_DEG を超えていれば「動いている」.
    動いているコマの |実測 − 指令| を「動いている途中の遅れ」とする (着いたと判定されたあとも動いていた分も含む).
  - 動いていないコマの |実測 − 指令| を「止まったあとのずれ」とする. 関節ごとに, 区間の最後の動いていないコマの値を
    その区間のずれとし (区間の数で数える. 長く待った区間が重くならない), run ごとに中央値・90%・最大を出す.
    codex R1 必須: 動いているかは関節ごとに見る (j1 が動き続けていても, 止まった j4 のずれを見落とさない).
    その関節が区間の中で一度も止まっていなければ, その区間のずれは無し (NaN) とする.
  - 区間の最後の「止まりが続いたところ」(関節ごと. つながった動いていないコマ) の変化 (最後 − 最初) を
    「止まっている間の変化」(じわじわ垂れる) とし, 続いた秒も出す. MOVE_EPS_DEG に満たない速さで動き続けるものはここに出る.
  - 前後 0.3 秒の窓で動きが見えなかったコマを「動いていない」としている (区間の端・まばらな記録では 1 コマでも
    動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
  - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.

ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)

log で見る (依頼 46 より前の記録とも比べられる):
  - 補正の 1 刻み: measured_after − after (全 6 関節)
  - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)

Usage:
  python3 pi05/diag_joint_hold.py --run pi05_20261101_190347 --run pi05_20261101_194042
  python3 pi05/diag_joint_hold.py --run ... --json docs/diag_joint_hold_20261101.json
"""
import argparse
import csv
import json
import sys
from pathlib import Path

import numpy as np

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
RUNS_ROOT = DATA_ROOT / "runs" / "pi05_policy"
JOINTS = ["j0", "j1", "j2", "j3", "j4", "j5"]
MOVE_WIN_SEC = 0.3      # 前後これだけの間の実測のふれで「動いている」を見る
MOVE_EPS_DEG = 0.3      # ふれがこれを超えたら動いている (読みの刻みは約 0.09°)
LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いていないコマがこれだけ続いた区間で見る
# 指令が変わらないのに動いた・止まったあとも残ったずれがこれを超えた区間を「外れ」として数える (依頼 57).
# 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
# Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
EVENT_DEG = 4.0
# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
FAST_RPC_SEC = 0.5
PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限

exec
/bin/zsh -lc "python3 -B - <<'PY'
import sys
sys.path.insert(0,'pi05')
import diag_joint_hold as d
import numpy as np

def seg(cmd, vals, fast=True, skip=None):
 q=np.zeros((len(vals),6)); q[:,4]=vals
 c=np.zeros(6); c[4]=cmd
 return dict(kind='send',stage='model',stages=['model']*len(vals),cmd=c,t_cmd=0,t0=0,ts=np.arange(len(vals))*.1,q=q,fast=fast,skip=skip)
a=seg(0,[0,1,2,3]); z=seg(0,[3,4,5,6])
for name,b in [('slow_empty',seg(0,[],False)),('changed_empty',seg(10,[])),('hold_empty',seg(0,[],skip=4)),('unknown',dict(seg(0,[3]),cmd=np.full(6,np.nan)))]:
 out=d.analyze_segments([a,b,z]); print(name,'away=',out[-1]['away_max'][4])
import test_diag_joint_hold as t
import unittest
unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(t.TestToday))
PY" in /Users/shimazueiichi/r2r2r-scripts
 exited 1 in 97ms:
zsh:1: can't create temp file for here document: operation not permitted

codex
pad の最近傍対応と、実測がある fast 区間同士の引き継ぎは意図に合っています。一方、実測がない区間では引き継ぎの解除処理も通らないため、指令変更や長押しを挟んだ場合をメモリ上で確認します。
exec
/bin/zsh -lc "python3 -B -c 'import sys; sys.path.insert(0,\"pi05\")
import diag_joint_hold as d
import numpy as np
def seg(cmd, vals, fast=True, skip=None):
 q=np.zeros((len(vals),6)); q[:,4]=vals
 c=np.zeros(6); c[4]=cmd
 return dict(kind=\"send\",stage=\"model\",stages=[\"model\"]*len(vals),cmd=c,t_cmd=0,t0=0,ts=np.arange(len(vals))*.1,q=q,fast=fast,skip=skip)
a=seg(0,[0,1,2,3]); z=seg(0,[3,4,5,6])
for name,b in [(\"slow_empty\",seg(0,[],False)),(\"changed_empty\",seg(10,[])),(\"hold_empty\",seg(0,[],skip=4)),(\"unknown\",dict(seg(0,[3]),cmd=np.full(6,np.nan)))]:
 out=d.analyze_segments([a,b,z]); print(name,\"away=\",out[-1][\"away_max\"][4])
import test_diag_joint_hold as t
import unittest
unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(t.TestToday))'" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 769ms:
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-is4N1Beg' (errno=Operation not permitted)
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-erfcBTpl' (errno=Operation not permitted)
test_20261106_gamepad (test_diag_joint_hold.TestToday)
依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ. ... ok
test_coarse_and_events (test_diag_joint_hold.TestToday) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.377s

OK
slow_empty away= 6.0
changed_empty away= 6.0
hold_empty away= 6.0
unknown away= 6.0

exec
/bin/zsh -lc "cat AGENTS.md; rg -n 'resends|t_send|t_halt' tools/gamepad_teleop.py; cat docs/review_request_20261106_corr_v5_gamepad_r2.md" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 107ms:
# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

## Project Overview

R2R2R (Real-to-Real-to-Real) is a robotic pick-and-place system using a **mechArm 270-Pi** robot arm controlled from a **Mac + Raspberry Pi** setup. The system uses camera-based red block detection with homography coordinate transformation to perform automated pick-and-place operations, with VLM (Codex API) verification of results.

## Architecture

**Two-machine split**: Mac handles vision/coordination, Pi controls the robot arm.

- **Mac side** (`camera_pick_mac.py`): Captures camera frames, detects red blocks via HSV color segmentation, converts pixel coords to robot coords using homography, then SSH+SCP to Pi to execute arm movements.
- **Pi side** (`camera_pick_pi.py`): Receives robot XY coordinates as CLI args, connects to mechArm via serial (`/dev/ttyAMA0` at 1000000 baud), executes pick sequence (ABOVE → APPROACH_PICK → PICK → grip → retract → HOME).
- **Loop runner** (`r2r2r_loop.py`): Orchestrates `pick_and_place_fixed.py` via SSH with up to 3 retries, uses VLM (`vlm_judge.py`) to verify success by checking if the red block landed in the target container.

### Key Modules

- `detect_block_position.py` — Red block detection (HSV thresholding) + `pixel_to_world()` homography transform. Imported by `camera_pick_mac.py`.
- `vlm_judge.py` — Captures a frame and sends to Codex API (Codex-sonnet-4-20250514) to judge if the block is in the target container. Imported by `r2r2r_loop.py`.
- `calibrate_homography.py` — Interactive 4-point calibration tool. Click 4 corners on camera view, input real-world mm coords, saves `homography.npy`.
- `pick_and_place_fixed.py` — Fixed-angle pick-and-place sequence (runs on Pi). Uses predefined joint angles, not camera-guided.

### Coordinate Pipeline

```
iPhone camera (Iriun Webcam, index 1)
  → pixel (u,v)
  → homography.npy (pixel → Robot mm)
  → Robot XY (mm) sent to Pi via SSH
  → Pi applies gripper offset (-20mm X, -80mm Y)
  → send_coords() with nearest calibration point's RXYZ
```

## Hardware Configuration

- **Robot**: mechArm 270-Pi, serial `/dev/ttyAMA0` @ 1000000 baud, `pymycobot` library
- **Camera**: Iriun Webcam (iPhone → Mac virtual camera), OpenCV index 1
- **SSH**: `ubuntu@192.168.2.1` via `~/.ssh/id_rsa_pi` (wired Ethernet)
- **Home position**: `send_angles([0, 0, 0, 0, 0, -30], 20)`
- **Gripper**: open = `set_gripper_state(0, 100)`, close = `set_gripper_state(1, 70)`

## Running Scripts

All scripts run as standalone Python3. Mac-side scripts require `opencv-python`, `numpy`, `paramiko`. Pi-side scripts require `pymycobot`.

```bash
# Camera-guided pick (run on Mac)
python3 camera_pick_mac.py

# Fixed-angle pick-and-place loop with VLM verification (run on Mac)
python3 r2r2r_loop.py

# Recalibrate homography (run on Mac, interactive)
python3 calibrate_homography.py

# Detect block position only (run on Mac)
python3 detect_block_position.py
```

## Current Phase (DAY151完了)
- Phase 1 ✅ 完了: r2r2r_loop.py で SUCCESS達成
- Phase 2 🔄 進行中: 成功率計測・CSV logging
- Phase 3 未着手: カメラベース失敗検知

## Lookup Table (FK Scan)
- fk_scan_motion_based.csv: 134,456点 (8姿勢起点 ±15°/5°刻り)
- fk_scan_table.csv: 481点 (PICK起点 j0全域)
- カラム構造: ee_x, ee_y, ee_z, j0, j1, j2, j3, j4, j5

## Next Task (DAY152)
Pi側 nearest-neighbor lookup実装:
  pixel → homography → Robot XY → fk_scan_motion_based.csvから最近傍 → send_angles()
補間関数:
  interpolate_angles(start, end, steps=5, easing=True)
  get_angles()で現在角度取得 → 5ステップ補間 → sleep=0.05s

## 禁止事項
- git clean 絶対禁止
- git add . 禁止（個別ファイル指定）
- homography.npy の値を変更しない
- ROI (1571, 607, 249, 211) 変更禁止
- time.sleep() を削除・短縮しない
- Calibration values in `calibration_reference.md` are measured/confirmed values — do not change without physical re-measurement
- Unity physics values (Stiffness: 20000, Damping: 2000) are locked
- Scripts transferred to Pi via SCP before each execution (always the latest local version)

## 記事制作ルール（SNS出力）
記事MD・JSONを出力した後、必ず以下をplain textで全部出力すること：
- X投稿文
- Facebook投稿文
- LinkedIn本文（英語→日本語）
- LinkedIn補足コメント（英語→日本語）

コードブロック不使用。省略禁止。

## AIツール分担（更新）
- Codex: コード実装・ファイル操作・ログ確認（メイン）
- Cline: どうしても必要な場合のみ（基本不使用）
- Codex.ai: 設計・ドキュメント・記事制作
- エイイチ: Unityエディタ操作・最終判断

## 物理憲法（追加ルール）

### send_coords() 使用禁止（DAY152で確定）
- mc.send_coords() の IK は RZ値が位置によって +167°〜−140° で暴れる
- ワークスペース全域で IK が不安定なため使用不可
- すべての動作は mc.send_angles() + lookup table 経由で実装する
- 例外なし。最適化や一時的な実験目的でも使用禁止
- 違反した場合、エイイチが即座に指摘・修正を要求

## FKスキャナー命名規則（DAY158で確定）

FKスキャナーのクラス名・ファイル名・GameObject名・ログプレフィックスは全て以下で統一する：

- `FKScanner_XPos` : Unity X正方向（赤ブロック側）、旧 LowerLeftFKScanner
- `FKScanner_XNeg` : Unity X負方向(白箱側)、旧 PlaceRightFKScanner
- `FKScanner_Central` : j0中央帯(補助)、旧 CentralFKScanner
- `FKScanner_MotionBased` : 運用ベース統合、旧 ParallelFKScanner

命名パターン:
- 単一軸指定:  `FKScanner_{軸}{Pos|Neg}`  例: `FKScanner_XPos`
- 複数軸指定:  `FKScanner_{軸1}{符号1}_{軸2}{符号2}`  例: `FKScanner_XPos_ZNeg`
- 意味ベース:  `FKScanner_{意味}`  例: `FKScanner_Central`, `FKScanner_MotionBased`

旧名（PlaceRight / LowerLeft 等の方向系命名）は**永久廃止**。
CSV出力名は互換性保持のため現行維持（`fk_scan_motion_based.csv` 等）。
Pi側 `r2r2r_loop.py` は `fk_scan_motion_based.csv` を参照する（リネーム対象外）。
# AGENTS.md 追記用：メソドロジー運用ルール

以下のセクションを既存の `AGENTS.md` の末尾または適切な位置に追記してください。

---

## メソドロジー運用ルール（DAY167以降）

Pixels to Motorsの実践から得られる判断軸・設計思想を、
他者が応用可能なメソドロジーとして蓄積する仕組み。

### 基本方針

- 目的：個人プロジェクトを「メソドロジー」として言語化し、将来的な発信・輸出の素材とする
- ペース：**週2回**（月・木を目安）
- 書けない日はスキップする（無理に絞り出さない）
- 実作業を圧迫しない範囲で運用する

### ファイル構成

```
~/r2r2r-content/methodology/
├── principles.md          # 原則の蓄積ファイル（Pxxx番号で管理）
└── day_log_template.md    # DAYログ用テンプレート（メソドロジー版）
```

### DAYログへの組み込み方

既存のDAYログフォーマットに、以下のセクションを追加する：

1. **判断ログ**（任意）: 複数の選択肢から1つを選んだ場合のみ記録
2. **今日の原則**（週2回程度）: 新規原則または既存原則の適用事例

### 新規原則の登録フロー

1. DAYログに「今日の原則」を1〜2文で記述
2. 分類タグを付与（`#design` / `#workflow` / `#tool` / `#safety` / `#ai-collab` / `#hardware` / `#documentation`）
3. `principles.md` にP{NNN}として転記
4. 既存原則と重複しないか確認（重複なら既存Pxxxの「適用例」への追記に切り替え）

### 既存原則の適用事例を記録するフロー

1. DAYログに `P{NNN}「原則名」が今日も効いた` と記述し、具体例を添える
2. `principles.md` の該当原則の「適用例」セクションにDAY番号と一行事例を追記

### 原則の抽象度ルール

- ❌ 低すぎる例：「FixedJointは安定する」（個別技術で終わる）
- ❌ 高すぎる例：「シンプルな設計が良い」（当たり前で新規性なし）
- ✅ 適切な例：「状況＋行動＋優先順位」の3要素を含む

### Codex / Codexへの依頼時のガイドライン

- DAYログを書く際、「メソドロジー版テンプレートに従って」と指示する
- 新規原則を登録する際は「principles.mdにP{次の番号}として追記」と明示
- 原則の重複チェックは、principles.md全体を読んでから判断する

### 禁止事項

- 原則を毎日書こうとしない（週2を死守しない）
- 原則のインフレ（量産）を避ける
- 実作業の時間を削ってまで原則を言語化しない
- 過去のDAYログを遡って原則を抽出する作業は**しない**（これから書くものだけ蓄積）

### 3ヶ月後の目標

- 原則数：20〜25個
- 次のマイルストーン：LinkedIn向けまとめコンテンツ
  `"The Pixels-to-Motors Method: N Principles for Designers Entering Physical AI"` を執筆可能な状態にする
484:            t_send = time.time()
488:                return {"sent": "unknown", "error": f"send_failed: {type(e).__name__}: {e}", "t_send": t_send}
491:            return {"sent": True, "reached": None, "no_check": True, "t_send": t_send}
497:        t_send = time.time()
503:                    "measured_before_send": _r(pre), "t_send": t_send}
507:        t_last_send = t0
511:        max_resends = STEP_MAX_RESENDS if step else IGNORED_MAX_RESENDS
514:        resends = []                      # 送り直した時刻 (送ってからの秒)
515:        base = {"sent": True, "timeout_sec": round(timeout, 2), "t_send": t_send, "measured_before_send": _r(pre),
516:                "resends": resends}
547:            if (len(resends) >= max_resends and moved < IGNORED_MOVE_DEG
548:                    and time.monotonic() - t_last_send >= check_sec):
552:            if (len(resends) < max_resends and moved < IGNORED_MOVE_DEG
553:                    and time.monotonic() - t_last_send >= check_sec):
564:                t_last_send = time.monotonic()
565:                resends.append(round(t_last_send - t0, 2))
576:        t_send = time.time()
577:        out = {"t_halt": t_send}
644:        t_send = time.time()
649:            return {"sent": True, "reached": None, "no_check": True, "t_send": t_send, "dry": True}
654:                "t_send": t_send, "dry": True}
816:                self.request_halt(reason, hold_id=h["id"], joint=h["joint"])
819:    def request_halt(self, reason, hold_id=None, joint=None):
877:            self.request_halt(reason)
1089:                        if (hs is not None and hs["resends"] < HOLD_MAX_RESENDS
1221:                        self.hold_sent["resends"] += 1
1226:                    self.hold_sent = {"q": list(mb), "last_q": list(mb), "t": time.monotonic(), "resends": 0}
1229:                self.request_halt("released_while_sending", hold_id=e["hold_id"], joint=job[1])
1273:            if res.get("resends"):
1274:                how += f"・無視されたので {len(res['resends'])} 回送り直した"
1605:                       "ignored_check_sec": IGNORED_CHECK_SEC, "ignored_max_resends": IGNORED_MAX_RESENDS,
# コードレビュー依頼 R2：diag_joint_hold の pad の組み方と away の引き継ぎ（2026-11-06 依頼 97）

R1（docs/review_result_20261106_corr_v5_gamepad_codex.md）の必須 2 件と推奨 1 件を pi05/diag_joint_hold.py に反映した。

1. 必須（pad 送信の対応付け）: `_pad_send_for(sends, used, ts)` を、まだ組にしていない送信のうち |t_send − ts| ≤ 0.2 秒でいちばん近いものを初めての送信とし、なければ ts より前に組にした送信のうちいちばん新しいものの送り直し（resends の数まで）とする形にした。どちらもなければ pad_unknown
2. 必須（区間をまたぐ垂れ）: すぐ返った区間がつづき、その関節の指令が変わらない間は、関節ごとに最小の |実測−指令| を区間をまたいで引き継ぐ（指令が変わる・すぐ返らない区間・長押しの関節では引き継がない）
3. 推奨（stop の対応）: halt もいちばん近い・まだ組にしていないものと一対一で組む

テスト: pi05/test_diag_joint_hold.py に 2 件（次の送信を拾わない・0→3° と 3→6° の垂れを 6° と見る／指令が変われば引き継がない）。21 件 OK。
11-01 の 9 本は前の結果と全部同じ。11-06 の 5 本: 入れる 4 本は外れ 0（j4 の離れた分の最大 3.8°（184452））、185554 は外れ 5 区間（j4）。docs/diag_joint_hold_20261106.txt・.json を作り直した。

差分は `git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py` で見られます。
ほかの推奨（184650 の範囲・1 コマの動き・偽物の assets）は報告に書いて据え置きます。

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

exec
/bin/zsh -lc "python3 -B -c 'import sys,json; sys.path.insert(0,\"pi05\"); import diag_joint_hold as d
for day in (\"20261101\",\"20261106\"):
 old=json.loads(open(\"docs/diag_joint_hold_\"+day+\".json\").read()); runs=old[\"runs\"]
 for stem,expected in runs.items():
  got,segs=d.summarize_run(d.RUNS_ROOT/stem)
  diff=[k for k,v in expected.items() if got.get(k)"'!=v]
  print(stem,"different_keys",diff,"events",got.get("event_segments"),"j4away",got.get("away_max",{}).get("j4"))
  if day=="20261106":
   barriers=[(s["kind"],s["fast"],s.get("skip")) for s in d.segments(d.load_rows(d.RUNS_ROOT/stem/"recording/states.csv"),[json.loads(l) for l in open(d.RUNS_ROOT/stem/"log.jsonl")]) if not len(s["ts"])]
   print("empty",barriers)
'"'" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 785ms:
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-le1nR0Y4' (errno=Operation not permitted)
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-EEwYBlWe' (errno=Operation not permitted)
pi05_20261101_190347 different_keys [] events 0 j4away 0.0
pi05_20261101_190854 different_keys [] events 0 j4away 0.0
pi05_20261101_191547 different_keys [] events 0 j4away 0.0
pi05_20261101_192101 different_keys [] events 0 j4away 0.0
pi05_20261101_192418 different_keys [] events 2 j4away 0.0
pi05_20261101_192758 different_keys [] events 2 j4away 0.0
pi05_20261101_193041 different_keys [] events 2 j4away 0.0
pi05_20261101_193411 different_keys [] events 3 j4away 0.0
pi05_20261101_194042 different_keys [] events 4 j4away 0.0
pi05_20261031_175527 different_keys [] events None j4away None
pi05_20261031_180435 different_keys [] events None j4away None
pi05_20261031_181946 different_keys [] events None j4away None
pi05_20261031_182545 different_keys [] events None j4away None
pi05_20261031_184016 different_keys [] events None j4away None
pi05_20261106_183926 different_keys [] events 0 j4away 1.23
empty []
pi05_20261106_184452 different_keys [] events 0 j4away 3.78
empty []
pi05_20261106_184650 different_keys [] events 0 j4away 2.73
empty []
pi05_20261106_184906 different_keys [] events 0 j4away 2.37
empty [('pad_hold', True, 1)]
pi05_20261106_185554 different_keys [] events 5 j4away 5.8
empty []

codex
**必須修正が1件残っています。** pad と stop の対応付けは修正意図に合っていますが、away の引き継ぎ解除が、実測のない区間で働きません。今回の5本の集計結果は再現できました。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| 実測のない区間での引き継ぎ解除 | **必須** | [analyze_segments](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:233) の carry 更新・解除が `if n ...` の内側にあります。このため、実測0件の「指令変更」「遅いRPC」「長押し」を挟んでも古い最小誤差が残ります。指令0°・実測0→3° → 実測なしの指令10° → 指令0°・実測3→6°で、最後の away が **3°ではなく6°**になることを再現しました。解除判定は実測件数に関係なく各区間で行い、指令変更・skip は該当関節、非fast・指令不明は全関節の carry を解除してください。 |
| 境界条件のテスト | **推奨** | 上記の実測0件の3条件と、`pad_unknown` を挟む場合を追加してください。現在の追加テストでは、実測のある区間での指令変更だけを確認しています。stop の最近傍・消費済みhaltの再利用禁止も回帰テストにすると有用です。 |
| pad送信の対応付け | 据え置き：修正確認 | 未使用・±0.2秒以内の最近傍を初回にし、なければ最新の対応済み送信へ再送数まで対応する実装になっています。初回＋再送数の回数管理も整合しています。 |
| 実測のあるfast区間のaway | 据え置き：修正確認 | 指令不変の関節で最小誤差を保持し、別関節の指令変更をまたいだ垂れを検出できます。長押し対象関節の除外も、実測がある区間では機能しています。 |
| stopの対応付け | 据え置き：修正確認 | 範囲内の最近傍・未使用haltを一対一で対応させています。既知のhalt_jointだけ指令を置き換える処理も維持されています。 |
| 11-01との互換性 | 据え置き：再確認済み | 9本について、既存JSONに保存された全結果項目が再計算と一致しました。 |
| 11-06の選別結果 | 据え置き：再現済み | 5本とも保存JSONと一致。採用4本は外れ0、184452のj4 away最大は3.78°。185554はj4の外れ5区間です。 |
| 184650の範囲・1コマの動き・偽物のassets | 据え置き | R1の推奨事項として残す扱いでよいです。今回の修正で解消した項目には含めません。 |

読み取り専用の実ログテスト2件は成功しました。全21件のうち一時ファイルを作るテストは実行せず、追加の境界条件はメモリ上で検証しました。ファイル変更、実機・Pod・Hugging Faceの操作は行っていません。
````

#### 3回目の依頼文 docs/review_request_20261106_corr_v5_gamepad_r3.md

```markdown
# コードレビュー依頼 R3：away の引き継ぎを実測のない区間でも切る（2026-11-06 依頼 97）

R2（docs/review_result_20261106_corr_v5_gamepad_codex_r2.md）の必須 1 件と推奨を pi05/diag_joint_hold.py に反映した。

1. 必須: analyze_segments で、引き継ぎを切るかを実測の有無に関係なく区間ごとに決めるようにした（すぐ返らない区間・指令の分からない区間 = 全関節を切る／指令が変わった関節・長押しの関節 = その関節を切る）。away の計算は切った関節（NaN）を区間の頭から数える
2. 推奨: テストを足した（実測のない区間を挟む 4 通り（指令が変わる・すぐ返らない・組にできない・j4 の長押し）では引き継がず、爪だけを挟むときは引き継ぐ／stop は一対一で組む）。pi05/test_diag_joint_hold.py 23 件 OK

11-01 の 9 本は前の結果と全部同じ。11-06 の 5 本も R2 のときと全部同じ値（入れる 4 本は外れ 0、185554 は外れ 5 区間）。

差分は `git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py` で見られます。

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

#### 3回目の結果 docs/review_result_20261106_corr_v5_gamepad_codex_r3.md

````markdown
# codex レビュー結果 R3：away の引き継ぎ（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad_r3.md（codex exec -s read-only）

差分とR2の指摘、関連する解析処理・テストを読み取りで確認します。ファイル変更や実機・外部サービスの操作は行いません。
exec
/bin/zsh -lc 'git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 117ms:
git: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
git: error: couldn't create cache file '/tmp/xcrun_db-NqP0WkpK' (errno=Operation not permitted)
git: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
git: error: couldn't create cache file '/tmp/xcrun_db-AmULmVQI' (errno=Operation not permitted)
diff --git a/pi05/diag_joint_hold.py b/pi05/diag_joint_hold.py
index b9f6122..45d90ee 100644
--- a/pi05/diag_joint_hold.py
+++ b/pi05/diag_joint_hold.py
@@ -23,6 +23,26 @@
     動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
   - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.
 
+ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
+  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
+    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
+    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
+      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
+      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
+                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
+      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
+                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
+    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
+  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
+    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
+      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
+      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
+        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
+        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
+      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
+        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
+    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)
+
 log で見る (依頼 46 より前の記録とも比べられる):
   - 補正の 1 刻み: measured_after − after (全 6 関節)
   - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)
@@ -49,6 +69,10 @@ LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いてい
 # 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
 # Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
 EVENT_DEG = 4.0
+# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
+# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
+FAST_RPC_SEC = 0.5
+PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限
 
 
 def load_rows(path):
@@ -81,22 +105,94 @@ def _rpc_sec(r):
     return 0.0
 
 
-def segments(rows):
-    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), ts[], q[], stages[]}]"""
+def pad_events(log):
+    """log.jsonl の type=pad の行から, 送ったもの (move / home) と止めたもの (halt) を時刻の順に集める (依頼 97)."""
+    sends, halts = [], []
+    for r in log or ():
+        if r.get("type") != "pad":
+            continue
+        if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None \
+                and r.get("t_send") is not None:
+            sends.append({"t": float(r["t_send"]), "cmd": np.asarray(r["command"][:6], float),
+                          "joint": r.get("joint") if r.get("kind") == "move" else None,
+                          "hold": bool(r.get("no_check")), "resends": len(r.get("resends") or []),
+                          "corr": r.get("correction_id")})
+        elif r.get("kind") == "halt" and r.get("t_halt") is not None:
+            q = r.get("measured_after_halt")
+            halts.append({"t": float(r["t_halt"]), "q": None if q is None else np.asarray(q[:6], float),
+                          "joint": r.get("halt_joint"), "how": r.get("halt"), "corr": r.get("correction_id")})
+    sends.sort(key=lambda x: x["t"]); halts.sort(key=lambda x: x["t"])
+    return sends, halts
+
+
+def _pad_send_for(sends, used, ts):
+    """send_angle の rpc 行 (時刻 ts) に当たる pad の送信と, それが何回目か (0 = 初めて・1 以上 = 送り直し).
+    codex R1 必須: 初めての送信は, まだ組にしていない送信のうち |t_send − ts| が PAD_MATCH_SEC 以内でいちばん近いもの.
+    それがなければ, ts より前に組にした送信のうち送り直しの残りがあるいちばん新しいものの送り直しとする.
+    どちらもなければ (None, 0). used = {送信の番号: 組にした回数}"""
+    cand = [(abs(x["t"] - ts), k) for k, x in enumerate(sends) if k not in used and abs(x["t"] - ts) <= PAD_MATCH_SEC]
+    if cand:
+        k = min(cand)[1]
+        used[k] = 1
+        return sends[k], 0
+    for k in sorted((k for k in used if sends[k]["t"] <= ts), key=lambda k: sends[k]["t"], reverse=True):
+        if used[k] <= sends[k]["resends"]:
+            used[k] += 1
+            return sends[k], used[k] - 1
+        break               # いちばん新しい送信の送り直しが尽きていれば, それより前のものにはしない
+    return None, 0
+
+
+def segments(rows, log=None):
+    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), fast, skip, ts[], q[], stages[]}]
+    log (log.jsonl の行) があれば, ゲームパッドの rpc 行 (send_angle / stop) の指令を pad の行から作る (依頼 97)."""
+    sends, halts = pad_events(log)
+    used_sends, used_halts = {}, set()
     segs, cur = [], None
+    nan6 = np.full(6, np.nan)
     for r in rows:
         ev = r.get("event_type")
         if ev == "send_angles":
             q = _q(r)
             if q is None:
                 continue
+            rs = _rpc_sec(r)
             cur = {"kind": "send", "stage": r.get("stage"), "cmd": q, "t_cmd": r["_ts"],
-                   "t0": r["_ts"] + _rpc_sec(r), "ts": [], "q": [], "stages": []}
+                   "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC, "skip": None,
+                   "ts": [], "q": [], "stages": []}
             segs.append(cur)
         elif ev == "rpc" and cur is not None:
             cmd = (r.get("note") or "").split()[0].replace("cmd=", "")
-            cur = {"kind": "grip_after", "grip_cmd": cmd, "stage": r.get("stage"), "cmd": cur["cmd"],
-                   "t_cmd": cur["t_cmd"], "t0": r["_ts"] + _rpc_sec(r), "ts": [], "q": [], "stages": []}
+            rs = _rpc_sec(r)
+            base = {"stage": r.get("stage"), "t_cmd": r["_ts"], "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC,
+                    "skip": None, "ts": [], "q": [], "stages": []}
+            if cmd == "send_angle":
+                x, n_used = _pad_send_for(sends, used_sends, r["_ts"])
+                if x is None:
+                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
+                else:
+                    cur = dict(base, kind="pad_hold" if x["hold"] else "pad_step", cmd=x["cmd"].copy(),
+                               skip=x["joint"] if x["hold"] else None, t_cmd=x["t"] if n_used == 0 else r["_ts"])
+            elif cmd == "stop":
+                # codex R1 推奨: いちばん近い・まだ組にしていない halt (一対一)
+                hc = [(abs(x["t"] - r["_ts"]), k) for k, x in enumerate(halts)
+                      if k not in used_halts and abs(x["t"] - r["_ts"]) <= PAD_MATCH_SEC]
+                h = None
+                if hc:
+                    used_halts.add(min(hc)[1])
+                    h = halts[min(hc)[1]]
+                if h is None or h["q"] is None:
+                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
+                else:
+                    c = np.asarray(cur["cmd"], float).copy()
+                    if h["joint"] is not None and not np.isnan(c).any():
+                        c[int(h["joint"])] = h["q"][int(h["joint"])]
+                    else:
+                        c = h["q"].copy()
+                    cur = dict(base, kind="pad_stop", cmd=c)
+            else:
+                cur = dict(base, kind="grip_after", grip_cmd=cmd, cmd=cur["cmd"], t_cmd=cur["t_cmd"],
+                           fast=cur.get("fast", False))
             segs.append(cur)
         elif ev == "actual" and cur is not None:
             q = _q(r)
@@ -123,17 +219,52 @@ def analyze_segments(segs):
     戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
     out = []
     nan6 = np.full(6, np.nan)
+    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
+    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
+    carry_cmd, carry_min = nan6.copy(), nan6.copy()
     for k, s in enumerate(segs):
         n = len(s["ts"])
         rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
                "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
                "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
-        if n:
+        rec["away_max"] = np.zeros(6)
+        rec["skip"] = s.get("skip")
+        rec["fast"] = bool(s.get("fast"))
+        # codex R2 必須: 引き継ぎを切るかは, 実測があるかに関係なく区間ごとに決める.
+        # すぐ返らない区間・指令の分からない区間は全関節, 指令が変わった関節・長押しの関節はその関節を切る
+        if not s.get("fast") or np.isnan(s["cmd"]).any():
+            carry_cmd, carry_min = nan6.copy(), nan6.copy()
+        else:
+            changed = ~np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0)
+            carry_min = np.where(changed, np.nan, carry_min)
+            carry_cmd = s["cmd"].copy()
+            if s.get("skip") is not None:
+                carry_min[int(s["skip"])] = np.nan
+        if n and not np.isnan(s["cmd"]).any():
             err = s["q"] - s["cmd"]
             mv = moving_mask(s["ts"], s["q"])
-            rec["lag_max"] = np.where(mv.any(axis=0), np.where(mv, np.abs(err), 0).max(axis=0), 0.0)
+            if s.get("fast"):
+                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
+                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
+                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
+                a = np.abs(err)
+                start = np.fmin(carry_min, a[0])                 # 切った関節 (NaN) は区間の頭から
+                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
+                rec["away_max"] = np.max(a - run_min, axis=0)
+                carry_min = run_min[-1].copy()
+                if s.get("skip") is not None:
+                    carry_min[int(s["skip"])] = np.nan
+                lag_mv = np.zeros_like(mv)
+            else:
+                lag_mv = mv
+            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
             for i in range(6):
+                if s.get("skip") is not None and i == int(s["skip"]):
+                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                 idx = np.flatnonzero(~mv[:, i])
+                if s.get("fast") and mv[:, i].any():
+                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
+                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                 if not len(idx):
                     continue
                 last = idx[-1]
@@ -143,7 +274,15 @@ def analyze_segments(segs):
                 rec["hold_err"][i] = err[last, i]
                 rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                 rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
-            rec["coarse_max"] = np.abs(err).max(axis=0)
+            if s.get("skip") is not None:
+                rec["lag_max"][int(s["skip"])] = 0.0
+                rec["away_max"][int(s["skip"])] = 0.0
+            ce = np.abs(err)
+            if s.get("skip") is not None:
+                ce[:, int(s["skip"])] = 0.0
+            rec["coarse_max"] = ce.max(axis=0)
+            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
+        elif n:
             rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
         out.append(rec)
     return out
@@ -200,7 +339,7 @@ def summarize_run(run_dir):
                 pass
     res = {"run": stem, "outcome": trial.get("outcome"), "corrections": trial.get("corrections")}
     rows = load_rows(run_dir / "recording" / "states.csv")
-    segs = analyze_segments(segments(rows))
+    segs = analyze_segments(segments(rows, log))
     res["has_send_rows"] = bool(segs)
     lh = log_hold_errors(log)
     res["log"] = {k: {j: _stats([e[i] for _, e in v]) for i, j in enumerate(JOINTS)} for k, v in lh.items()}
@@ -235,13 +374,21 @@ def summarize_run(run_dir):
     for s in work:
         for i, j in enumerate(JOINTS):
             he = float(s["hold_err"][i]) if _has(s["hold_err"][i]) else None
-            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG:
-                ev.append({"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
-                           "joint": j, "cmd": round(float(s["cmd"][i]), 2),
-                           "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
-                           "hold_err": None if he is None else round(he, 2),
-                           "lag_max": round(float(s["lag_max"][i]), 2)})
+            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG or s["away_max"][i] > EVENT_DEG:
+                e = {"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
+                     "joint": j, "cmd": round(float(s["cmd"][i]), 2),
+                     "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
+                     "hold_err": None if he is None else round(he, 2),
+                     "lag_max": round(float(s["lag_max"][i]), 2)}
+                if s["away_max"][i] > 0:
+                    e["away_max"] = round(float(s["away_max"][i]), 2)      # 依頼 97 (送ったらすぐ返った区間だけ)
+                ev.append(e)
     res["events"] = ev
+    # 依頼 97: 送ったらすぐ返った区間の数・ゲームパッドの区間の数・組にできなかった rpc 行の数
+    res["fast_segments"] = sum(1 for s in segs if s.get("fast"))
+    res["pad_segments"] = {k: sum(1 for s in segs if s["kind"] == k) for k in ("pad_step", "pad_hold", "pad_stop",
+                                                                             "pad_unknown")}
+    res["away_max"] = {j: round(float(max([s["away_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}
     res["event_segments"] = len({e["seg"] for e in ev})   # codex R1・R2 推奨: 区間の数 (区間の番号で数える. 関節は重ねない)
     res["drift_max"] = {j: round(float(max([abs(s["drift"][i]) for s in work
                                             if _has(s["drift"][i]) and s["still_sec"][i] >= LONG_HOLD_SEC] or [0.0])), 2)
@@ -290,7 +437,7 @@ def main():
     if str(root).startswith("/Volumes/KIOXIA") and not Path("/Volumes/KIOXIA").is_mount():
         sys.exit("[ABORT] KIOXIA not mounted: /Volumes/KIOXIA")
     out = {"params": {"move_win_sec": MOVE_WIN_SEC, "move_eps_deg": MOVE_EPS_DEG, "event_deg": EVENT_DEG,
-                      "long_hold_sec": LONG_HOLD_SEC}, "runs": {}, "timelines": {}}
+                      "long_hold_sec": LONG_HOLD_SEC, "fast_rpc_sec": FAST_RPC_SEC}, "runs": {}, "timelines": {}}
     for stem in args.run:
         res, segs = summarize_run(root / stem)
         out["runs"][stem] = res
@@ -308,11 +455,15 @@ def main():
             print("   止まり中央 " + _h("median"))
             print("   止まり符号 " + " ".join(f"{j}={'-' if v is None else f'{v:+.1f}':>5}" for j, v in (res["hold_signed_median"] or {}).items()))
             print("   じわじわ   " + " ".join(f"{j}={v:5.1f}" for j, v in res["drift_max"].items()))
+            if res["fast_segments"]:
+                print("   向かう途中に離れた " + " ".join(f"{j}={v:5.1f}" for j, v in res["away_max"].items())
+                      + f"  (すぐ返った区間 {res['fast_segments']}・ゲームパッド {res['pad_segments']})")
             print(f"   段ごと {res['hold_by_context']}")
             print(f"   外れ (>{EVENT_DEG:.0f}°) {res['event_segments']} 区間 ({len(res['events'])} 区間・関節の組)")
             for e in res["events"]:
                 print(f"     {e['sec']:6.1f}秒 {e['stage']:7s} {e['kind']:10s} {e['joint']} 指令 {e['cmd']:7.2f} "
-                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}")
+                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}"
+                      + (f"  離れた {e['away_max']}" if "away_max" in e else ""))
             for gr in res["grip"]:
                 print(f"   爪 {gr['cmd']} ({gr['stage']}) 前後の変化 {gr['delta']}  爪のあとのずれ {gr['err_after']}")
             for h in res["home"]:
diff --git a/pi05/test_diag_joint_hold.py b/pi05/test_diag_joint_hold.py
index 2efe3cb..6ed92e3 100644
--- a/pi05/test_diag_joint_hold.py
+++ b/pi05/test_diag_joint_hold.py
@@ -6,6 +6,7 @@
   - 爪の rpc で区間を切る
   - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
   - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
+  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
 """
 import csv
 import json
@@ -33,6 +34,9 @@ class Rec:
     def send(self, t, q, stage="model", rpc=1.5):
         self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})
 
+    def rpc(self, t, cmd, stage="correct", rpc=0.03):
+        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})
+
     def grip(self, t, stage="model", rpc=1.5):
         self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})
 
@@ -223,6 +227,184 @@ class TestSegments(unittest.TestCase):
         self.assertEqual(segs, [])
 
 
+def pad_move(t, command, joint, hold=False, resends=(), cid=1):
+    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
+         "joint": joint, "resends": list(resends)}
+    if hold:
+        r["no_check"] = True
+    return r
+
+
+def pad_halt(t, q, joint, cid=1):
+    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
+            "halt_joint": joint, "halt": "stop"}
+
+
+class TestPadAndFast(unittest.TestCase):
+    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""
+
+    def build(self, rec, log=()):
+        td = tempfile.mkdtemp()
+        run = Path(td) / "pi05_p"
+        rec.write(run, log)
+        return d.summarize_run(run)
+
+    def test_fast_approach_is_not_lag(self):
+        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
+        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
+        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
+        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
+        r.actual(2.3, 3.5, pose(-59.5))
+        res, segs = self.build(r)
+        self.assertEqual(res["fast_segments"], 1)
+        self.assertEqual(res["events"], [])
+        self.assertEqual(res["lag_max"]["j4"], 0.0)
+        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
+        self.assertLess(res["away_max"]["j4"], 0.2)
+
+    def test_fast_hold_ignores_still_before_moving(self):
+        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 0.8, pose(-50))
+        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
+        res, segs = self.build(r)
+        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
+        self.assertEqual(res["events"], [])
+
+    def test_fast_sag_after_arrival_is_event(self):
+        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 1.0, pose(-60))
+        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
+        r.actual(1.6, 3.0, pose(-54))
+        res, segs = self.build(r)
+        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
+        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
+        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)
+
+    def test_pad_step_hold_stop(self):
+        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
+        start = pose(-60, j1=40)
+        step = pose(-60, j1=40); step[0] = 1.0
+        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
+        r = Rec()
+        r.send(0.0, start, stage="model", rpc=0.03)
+        r.actual(0.03, 1.0, start, stage="model")
+        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
+        r.actual(2.03, 3.0, step, stage="correct")
+        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)
+        r.actual(3.03, 3.5, lambda t: [1.0, 40 - 20 * (t - 3.03), 0, 0, -60, -30], stage="correct")
+        r.rpc(3.5, "stop")
+        r.actual(3.55, 5.0, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
+        r.rpc(5.0, "send_angle")                                               # 組にできない (pad の行がない… 送り直しの数を越える)
+        r.actual(5.03, 5.5, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
+        log = [pad_move(2.0, step, 0), pad_move(3.0, far, 1, hold=True), pad_halt(3.5, [1.0, 30.6, 0, 0, -60.2, -30], 1)]
+        res, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_step", "pad_hold", "pad_stop", "pad_unknown"])
+        self.assertEqual(res["pad_segments"], {"pad_step": 1, "pad_hold": 1, "pad_stop": 1, "pad_unknown": 1})
+        np.testing.assert_allclose(segs[1]["cmd"], step)
+        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
+        self.assertEqual(segs[2]["lag_max"][1], 0.0)
+        self.assertEqual(segs[2]["skip"], 1)
+        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
+        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
+        self.assertEqual(res["events"], [])
+
+    def test_pad_resend_and_uncommanded_wrist(self):
+        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
+        start = pose(-60, j1=50)
+        far = pose(-60, j1=35)
+        r = Rec()
+        r.send(0.0, start, rpc=0.03)
+        r.actual(0.03, 1.0, start)
+        r.rpc(2.0, "send_angle")
+        r.rpc(2.3, "send_angle")                                               # 送り直し
+        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
+        r.rpc(3.0, "stop")
+        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
+        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
+        res, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
+        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
+        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
+        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)
+
+    def test_pad_send_not_matched_to_next(self):
+        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
+        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
+        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
+        r = Rec()
+        r.send(0.0, pose(-60, j1=40), rpc=0.03)
+        r.actual(0.03, 0.9, pose(-60, j1=40))
+        r.rpc(1.0, "send_angle")
+        r.actual(1.03, 1.14, a_cmd, stage="correct")
+        r.rpc(1.16, "send_angle")
+        r.actual(1.19, 2.0, b_cmd, stage="correct")
+        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
+        _, segs = self.build(r, log)
+        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
+        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)
+
+    def test_away_carries_across_fast_segments(self):
+        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
+        r = Rec()
+        r.send(0.0, pose(-60, j1=10), rpc=0.03)
+        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
+        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
+        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
+        res, segs = self.build(r)
+        self.assertLess(segs[0]["away_max"][4], 3.1)
+        self.assertGreater(segs[1]["away_max"][4], 5.5)
+        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
+        r2 = Rec()                                          # j4 の指令が変われば引き継がない
+        r2.send(0.0, pose(-60), rpc=0.03)
+        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
+        r2.send(1.0, pose(-57), rpc=0.03)
+        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
+        res2, _ = self.build(r2)
+        self.assertEqual(res2["events"], [])
+
+    def test_carry_cut_by_segments_without_samples(self):
+        """codex R2 必須: 実測のない区間 (指令が変わる・すぐ返らない・長押し・組にできない) を挟めば引き継がない.
+        指令 0°・実測 0→3° → (実測なしの区間) → 指令 0°・実測 3→6° で, 最後の away は 3° (6° にしない)."""
+        def run(between, log=()):
+            r = Rec()
+            r.send(0.0, pose(-60), rpc=0.03)
+            r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
+            between(r)
+            r.send(1.2, pose(-60), rpc=0.03)
+            r.actual(1.23, 2.2, lambda t: pose(-60 + 3 * (t - 0.2)))
+            _, segs = self.build(r, log)
+            return segs[-1]["away_max"][4]
+        self.assertLess(run(lambda r: r.send(1.0, pose(-50), rpc=0.03)), 3.1)              # 指令が変わる
+        self.assertLess(run(lambda r: r.send(1.0, pose(-60), rpc=1.5)), 3.1)               # すぐ返らない
+        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model")), 3.1)       # 組にできない
+        far = pose(-45)
+        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model"),
+                            [pad_move(1.0, far, 4, hold=True)]), 3.1)                        # j4 の長押し
+        self.assertGreater(run(lambda r: r.grip(1.0, rpc=0.01)), 5.5)                      # 爪だけ: 引き継ぐ
+
+    def test_halt_one_to_one(self):
+        """codex R1 推奨: stop はいちばん近い・まだ組にしていない halt と組む (同じ halt を 2 回使わない)."""
+        r = Rec()
+        r.send(0.0, pose(-60, j1=40), rpc=0.03)
+        r.actual(0.03, 0.9, pose(-60, j1=40))
+        r.rpc(1.0, "stop", stage="correct")
+        r.actual(1.03, 1.1, pose(-60, j1=35), stage="correct")
+        r.rpc(1.15, "stop", stage="correct")
+        r.actual(1.18, 2.0, pose(-60, j1=30), stage="correct")
+        log = [pad_halt(1.0, pose(-60, j1=35), 1), pad_halt(1.15, pose(-60, j1=30), 1)]
+        _, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_stop", "pad_stop"])
+        self.assertAlmostEqual(segs[1]["cmd"][1], 35.0)
+        self.assertAlmostEqual(segs[2]["cmd"][1], 30.0)
+
+
 RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")
 
 
@@ -242,6 +424,21 @@ class TestToday(unittest.TestCase):
             else:
                 self.assertGreater(n_ev, 0, stem)
 
+    def test_20261106_gamepad(self):
+        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
+        for stem in ("183926", "184452", "184650", "184906", "185554"):
+            p = RUNS / f"pi05_20261106_{stem}"
+            if not p.is_dir():
+                self.skipTest("11-06 の記録がない")
+            res, _ = d.summarize_run(p)
+            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
+            self.assertGreater(res["fast_segments"], 0, stem)
+            if stem == "185554":
+                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
+                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
+            else:
+                self.assertEqual(res["events"], [], stem)
+
 
 if __name__ == "__main__":
     unittest.main()

exec
/bin/zsh -lc 'cat docs/review_result_20261106_corr_v5_gamepad_codex_r2.md' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 74ms:
# codex レビュー結果 R2：pad の組み方と away の引き継ぎ（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad_r2.md（codex exec -s read-only）

差分、R1の指摘、テストと再生成レポートを読み取りだけで確認します。pad の対応付けと関節ごとの引き継ぎ条件を中心にレビューします。
exec
/bin/zsh -lc 'git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 127ms:
git: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
git: error: couldn't create cache file '/tmp/xcrun_db-4DMDrhxj' (errno=Operation not permitted)
git: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
git: error: couldn't create cache file '/tmp/xcrun_db-YIBxMFZR' (errno=Operation not permitted)
diff --git a/pi05/diag_joint_hold.py b/pi05/diag_joint_hold.py
index b9f6122..0200f31 100644
--- a/pi05/diag_joint_hold.py
+++ b/pi05/diag_joint_hold.py
@@ -23,6 +23,26 @@
     動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
   - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.
 
+ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
+  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
+    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
+    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
+      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
+      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
+                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
+      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
+                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
+    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
+  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
+    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
+      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
+      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
+        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
+        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
+      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
+        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
+    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)
+
 log で見る (依頼 46 より前の記録とも比べられる):
   - 補正の 1 刻み: measured_after − after (全 6 関節)
   - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)
@@ -49,6 +69,10 @@ LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いてい
 # 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
 # Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
 EVENT_DEG = 4.0
+# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
+# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
+FAST_RPC_SEC = 0.5
+PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限
 
 
 def load_rows(path):
@@ -81,22 +105,94 @@ def _rpc_sec(r):
     return 0.0
 
 
-def segments(rows):
-    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), ts[], q[], stages[]}]"""
+def pad_events(log):
+    """log.jsonl の type=pad の行から, 送ったもの (move / home) と止めたもの (halt) を時刻の順に集める (依頼 97)."""
+    sends, halts = [], []
+    for r in log or ():
+        if r.get("type") != "pad":
+            continue
+        if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None \
+                and r.get("t_send") is not None:
+            sends.append({"t": float(r["t_send"]), "cmd": np.asarray(r["command"][:6], float),
+                          "joint": r.get("joint") if r.get("kind") == "move" else None,
+                          "hold": bool(r.get("no_check")), "resends": len(r.get("resends") or []),
+                          "corr": r.get("correction_id")})
+        elif r.get("kind") == "halt" and r.get("t_halt") is not None:
+            q = r.get("measured_after_halt")
+            halts.append({"t": float(r["t_halt"]), "q": None if q is None else np.asarray(q[:6], float),
+                          "joint": r.get("halt_joint"), "how": r.get("halt"), "corr": r.get("correction_id")})
+    sends.sort(key=lambda x: x["t"]); halts.sort(key=lambda x: x["t"])
+    return sends, halts
+
+
+def _pad_send_for(sends, used, ts):
+    """send_angle の rpc 行 (時刻 ts) に当たる pad の送信と, それが何回目か (0 = 初めて・1 以上 = 送り直し).
+    codex R1 必須: 初めての送信は, まだ組にしていない送信のうち |t_send − ts| が PAD_MATCH_SEC 以内でいちばん近いもの.
+    それがなければ, ts より前に組にした送信のうち送り直しの残りがあるいちばん新しいものの送り直しとする.
+    どちらもなければ (None, 0). used = {送信の番号: 組にした回数}"""
+    cand = [(abs(x["t"] - ts), k) for k, x in enumerate(sends) if k not in used and abs(x["t"] - ts) <= PAD_MATCH_SEC]
+    if cand:
+        k = min(cand)[1]
+        used[k] = 1
+        return sends[k], 0
+    for k in sorted((k for k in used if sends[k]["t"] <= ts), key=lambda k: sends[k]["t"], reverse=True):
+        if used[k] <= sends[k]["resends"]:
+            used[k] += 1
+            return sends[k], used[k] - 1
+        break               # いちばん新しい送信の送り直しが尽きていれば, それより前のものにはしない
+    return None, 0
+
+
+def segments(rows, log=None):
+    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), fast, skip, ts[], q[], stages[]}]
+    log (log.jsonl の行) があれば, ゲームパッドの rpc 行 (send_angle / stop) の指令を pad の行から作る (依頼 97)."""
+    sends, halts = pad_events(log)
+    used_sends, used_halts = {}, set()
     segs, cur = [], None
+    nan6 = np.full(6, np.nan)
     for r in rows:
         ev = r.get("event_type")
         if ev == "send_angles":
             q = _q(r)
             if q is None:
                 continue
+            rs = _rpc_sec(r)
             cur = {"kind": "send", "stage": r.get("stage"), "cmd": q, "t_cmd": r["_ts"],
-                   "t0": r["_ts"] + _rpc_sec(r), "ts": [], "q": [], "stages": []}
+                   "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC, "skip": None,
+                   "ts": [], "q": [], "stages": []}
             segs.append(cur)
         elif ev == "rpc" and cur is not None:
             cmd = (r.get("note") or "").split()[0].replace("cmd=", "")
-            cur = {"kind": "grip_after", "grip_cmd": cmd, "stage": r.get("stage"), "cmd": cur["cmd"],
-                   "t_cmd": cur["t_cmd"], "t0": r["_ts"] + _rpc_sec(r), "ts": [], "q": [], "stages": []}
+            rs = _rpc_sec(r)
+            base = {"stage": r.get("stage"), "t_cmd": r["_ts"], "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC,
+                    "skip": None, "ts": [], "q": [], "stages": []}
+            if cmd == "send_angle":
+                x, n_used = _pad_send_for(sends, used_sends, r["_ts"])
+                if x is None:
+                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
+                else:
+                    cur = dict(base, kind="pad_hold" if x["hold"] else "pad_step", cmd=x["cmd"].copy(),
+                               skip=x["joint"] if x["hold"] else None, t_cmd=x["t"] if n_used == 0 else r["_ts"])
+            elif cmd == "stop":
+                # codex R1 推奨: いちばん近い・まだ組にしていない halt (一対一)
+                hc = [(abs(x["t"] - r["_ts"]), k) for k, x in enumerate(halts)
+                      if k not in used_halts and abs(x["t"] - r["_ts"]) <= PAD_MATCH_SEC]
+                h = None
+                if hc:
+                    used_halts.add(min(hc)[1])
+                    h = halts[min(hc)[1]]
+                if h is None or h["q"] is None:
+                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
+                else:
+                    c = np.asarray(cur["cmd"], float).copy()
+                    if h["joint"] is not None and not np.isnan(c).any():
+                        c[int(h["joint"])] = h["q"][int(h["joint"])]
+                    else:
+                        c = h["q"].copy()
+                    cur = dict(base, kind="pad_stop", cmd=c)
+            else:
+                cur = dict(base, kind="grip_after", grip_cmd=cmd, cmd=cur["cmd"], t_cmd=cur["t_cmd"],
+                           fast=cur.get("fast", False))
             segs.append(cur)
         elif ev == "actual" and cur is not None:
             q = _q(r)
@@ -123,17 +219,44 @@ def analyze_segments(segs):
     戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
     out = []
     nan6 = np.full(6, np.nan)
+    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
+    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
+    carry_cmd, carry_min = nan6.copy(), nan6.copy()
     for k, s in enumerate(segs):
         n = len(s["ts"])
         rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
                "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
                "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
-        if n:
+        rec["away_max"] = np.zeros(6)
+        rec["skip"] = s.get("skip")
+        rec["fast"] = bool(s.get("fast"))
+        if n and not np.isnan(s["cmd"]).any():
             err = s["q"] - s["cmd"]
             mv = moving_mask(s["ts"], s["q"])
-            rec["lag_max"] = np.where(mv.any(axis=0), np.where(mv, np.abs(err), 0).max(axis=0), 0.0)
+            if s.get("fast"):
+                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
+                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
+                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
+                a = np.abs(err)
+                same = np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0) & ~np.isnan(carry_min)
+                start = np.where(same, np.fmin(carry_min, a[0]), a[0])
+                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
+                rec["away_max"] = np.max(a - run_min, axis=0)
+                carry_cmd, carry_min = s["cmd"].copy(), run_min[-1].copy()
+                if s.get("skip") is not None:
+                    carry_min[int(s["skip"])] = np.nan
+                lag_mv = np.zeros_like(mv)
+            else:
+                lag_mv = mv
+                carry_cmd, carry_min = nan6.copy(), nan6.copy()
+            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
             for i in range(6):
+                if s.get("skip") is not None and i == int(s["skip"]):
+                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                 idx = np.flatnonzero(~mv[:, i])
+                if s.get("fast") and mv[:, i].any():
+                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
+                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                 if not len(idx):
                     continue
                 last = idx[-1]
@@ -143,7 +266,15 @@ def analyze_segments(segs):
                 rec["hold_err"][i] = err[last, i]
                 rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                 rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
-            rec["coarse_max"] = np.abs(err).max(axis=0)
+            if s.get("skip") is not None:
+                rec["lag_max"][int(s["skip"])] = 0.0
+                rec["away_max"][int(s["skip"])] = 0.0
+            ce = np.abs(err)
+            if s.get("skip") is not None:
+                ce[:, int(s["skip"])] = 0.0
+            rec["coarse_max"] = ce.max(axis=0)
+            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
+        elif n:
             rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
         out.append(rec)
     return out
@@ -200,7 +331,7 @@ def summarize_run(run_dir):
                 pass
     res = {"run": stem, "outcome": trial.get("outcome"), "corrections": trial.get("corrections")}
     rows = load_rows(run_dir / "recording" / "states.csv")
-    segs = analyze_segments(segments(rows))
+    segs = analyze_segments(segments(rows, log))
     res["has_send_rows"] = bool(segs)
     lh = log_hold_errors(log)
     res["log"] = {k: {j: _stats([e[i] for _, e in v]) for i, j in enumerate(JOINTS)} for k, v in lh.items()}
@@ -235,13 +366,21 @@ def summarize_run(run_dir):
     for s in work:
         for i, j in enumerate(JOINTS):
             he = float(s["hold_err"][i]) if _has(s["hold_err"][i]) else None
-            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG:
-                ev.append({"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
-                           "joint": j, "cmd": round(float(s["cmd"][i]), 2),
-                           "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
-                           "hold_err": None if he is None else round(he, 2),
-                           "lag_max": round(float(s["lag_max"][i]), 2)})
+            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG or s["away_max"][i] > EVENT_DEG:
+                e = {"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
+                     "joint": j, "cmd": round(float(s["cmd"][i]), 2),
+                     "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
+                     "hold_err": None if he is None else round(he, 2),
+                     "lag_max": round(float(s["lag_max"][i]), 2)}
+                if s["away_max"][i] > 0:
+                    e["away_max"] = round(float(s["away_max"][i]), 2)      # 依頼 97 (送ったらすぐ返った区間だけ)
+                ev.append(e)
     res["events"] = ev
+    # 依頼 97: 送ったらすぐ返った区間の数・ゲームパッドの区間の数・組にできなかった rpc 行の数
+    res["fast_segments"] = sum(1 for s in segs if s.get("fast"))
+    res["pad_segments"] = {k: sum(1 for s in segs if s["kind"] == k) for k in ("pad_step", "pad_hold", "pad_stop",
+                                                                             "pad_unknown")}
+    res["away_max"] = {j: round(float(max([s["away_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}
     res["event_segments"] = len({e["seg"] for e in ev})   # codex R1・R2 推奨: 区間の数 (区間の番号で数える. 関節は重ねない)
     res["drift_max"] = {j: round(float(max([abs(s["drift"][i]) for s in work
                                             if _has(s["drift"][i]) and s["still_sec"][i] >= LONG_HOLD_SEC] or [0.0])), 2)
@@ -290,7 +429,7 @@ def main():
     if str(root).startswith("/Volumes/KIOXIA") and not Path("/Volumes/KIOXIA").is_mount():
         sys.exit("[ABORT] KIOXIA not mounted: /Volumes/KIOXIA")
     out = {"params": {"move_win_sec": MOVE_WIN_SEC, "move_eps_deg": MOVE_EPS_DEG, "event_deg": EVENT_DEG,
-                      "long_hold_sec": LONG_HOLD_SEC}, "runs": {}, "timelines": {}}
+                      "long_hold_sec": LONG_HOLD_SEC, "fast_rpc_sec": FAST_RPC_SEC}, "runs": {}, "timelines": {}}
     for stem in args.run:
         res, segs = summarize_run(root / stem)
         out["runs"][stem] = res
@@ -308,11 +447,15 @@ def main():
             print("   止まり中央 " + _h("median"))
             print("   止まり符号 " + " ".join(f"{j}={'-' if v is None else f'{v:+.1f}':>5}" for j, v in (res["hold_signed_median"] or {}).items()))
             print("   じわじわ   " + " ".join(f"{j}={v:5.1f}" for j, v in res["drift_max"].items()))
+            if res["fast_segments"]:
+                print("   向かう途中に離れた " + " ".join(f"{j}={v:5.1f}" for j, v in res["away_max"].items())
+                      + f"  (すぐ返った区間 {res['fast_segments']}・ゲームパッド {res['pad_segments']})")
             print(f"   段ごと {res['hold_by_context']}")
             print(f"   外れ (>{EVENT_DEG:.0f}°) {res['event_segments']} 区間 ({len(res['events'])} 区間・関節の組)")
             for e in res["events"]:
                 print(f"     {e['sec']:6.1f}秒 {e['stage']:7s} {e['kind']:10s} {e['joint']} 指令 {e['cmd']:7.2f} "
-                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}")
+                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}"
+                      + (f"  離れた {e['away_max']}" if "away_max" in e else ""))
             for gr in res["grip"]:
                 print(f"   爪 {gr['cmd']} ({gr['stage']}) 前後の変化 {gr['delta']}  爪のあとのずれ {gr['err_after']}")
             for h in res["home"]:
diff --git a/pi05/test_diag_joint_hold.py b/pi05/test_diag_joint_hold.py
index 2efe3cb..512cbe8 100644
--- a/pi05/test_diag_joint_hold.py
+++ b/pi05/test_diag_joint_hold.py
@@ -6,6 +6,7 @@
   - 爪の rpc で区間を切る
   - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
   - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
+  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
 """
 import csv
 import json
@@ -33,6 +34,9 @@ class Rec:
     def send(self, t, q, stage="model", rpc=1.5):
         self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})
 
+    def rpc(self, t, cmd, stage="correct", rpc=0.03):
+        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})
+
     def grip(self, t, stage="model", rpc=1.5):
         self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})
 
@@ -223,6 +227,149 @@ class TestSegments(unittest.TestCase):
         self.assertEqual(segs, [])
 
 
+def pad_move(t, command, joint, hold=False, resends=(), cid=1):
+    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
+         "joint": joint, "resends": list(resends)}
+    if hold:
+        r["no_check"] = True
+    return r
+
+
+def pad_halt(t, q, joint, cid=1):
+    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
+            "halt_joint": joint, "halt": "stop"}
+
+
+class TestPadAndFast(unittest.TestCase):
+    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""
+
+    def build(self, rec, log=()):
+        td = tempfile.mkdtemp()
+        run = Path(td) / "pi05_p"
+        rec.write(run, log)
+        return d.summarize_run(run)
+
+    def test_fast_approach_is_not_lag(self):
+        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
+        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
+        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
+        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
+        r.actual(2.3, 3.5, pose(-59.5))
+        res, segs = self.build(r)
+        self.assertEqual(res["fast_segments"], 1)
+        self.assertEqual(res["events"], [])
+        self.assertEqual(res["lag_max"]["j4"], 0.0)
+        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
+        self.assertLess(res["away_max"]["j4"], 0.2)
+
+    def test_fast_hold_ignores_still_before_moving(self):
+        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 0.8, pose(-50))
+        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
+        res, segs = self.build(r)
+        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
+        self.assertEqual(res["events"], [])
+
+    def test_fast_sag_after_arrival_is_event(self):
+        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
+        r = Rec()
+        r.send(0.0, pose(-60), rpc=0.03)
+        r.actual(0.03, 1.0, pose(-60))
+        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
+        r.actual(1.6, 3.0, pose(-54))
+        res, segs = self.build(r)
+        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
+        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
+        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)
+
+    def test_pad_step_hold_stop(self):
+        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
+        start = pose(-60, j1=40)
+        step = pose(-60, j1=40); step[0] = 1.0
+        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
+        r = Rec()
+        r.send(0.0, start, stage="model", rpc=0.03)
+        r.actual(0.03, 1.0, start, stage="model")
+        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
+        r.actual(2.03, 3.0, step, stage="correct")
+        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)
+        r.actual(3.03, 3.5, lambda t: [1.0, 40 - 20 * (t - 3.03), 0, 0, -60, -30], stage="correct")
+        r.rpc(3.5, "stop")
+        r.actual(3.55, 5.0, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
+        r.rpc(5.0, "send_angle")                                               # 組にできない (pad の行がない… 送り直しの数を越える)
+        r.actual(5.03, 5.5, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
+        log = [pad_move(2.0, step, 0), pad_move(3.0, far, 1, hold=True), pad_halt(3.5, [1.0, 30.6, 0, 0, -60.2, -30], 1)]
+        res, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_step", "pad_hold", "pad_stop", "pad_unknown"])
+        self.assertEqual(res["pad_segments"], {"pad_step": 1, "pad_hold": 1, "pad_stop": 1, "pad_unknown": 1})
+        np.testing.assert_allclose(segs[1]["cmd"], step)
+        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
+        self.assertEqual(segs[2]["lag_max"][1], 0.0)
+        self.assertEqual(segs[2]["skip"], 1)
+        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
+        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
+        self.assertEqual(res["events"], [])
+
+    def test_pad_resend_and_uncommanded_wrist(self):
+        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
+        start = pose(-60, j1=50)
+        far = pose(-60, j1=35)
+        r = Rec()
+        r.send(0.0, start, rpc=0.03)
+        r.actual(0.03, 1.0, start)
+        r.rpc(2.0, "send_angle")
+        r.rpc(2.3, "send_angle")                                               # 送り直し
+        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
+        r.rpc(3.0, "stop")
+        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
+        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
+        res, segs = self.build(r, log)
+        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
+        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
+        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
+        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)
+
+    def test_pad_send_not_matched_to_next(self):
+        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
+        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
+        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
+        r = Rec()
+        r.send(0.0, pose(-60, j1=40), rpc=0.03)
+        r.actual(0.03, 0.9, pose(-60, j1=40))
+        r.rpc(1.0, "send_angle")
+        r.actual(1.03, 1.14, a_cmd, stage="correct")
+        r.rpc(1.16, "send_angle")
+        r.actual(1.19, 2.0, b_cmd, stage="correct")
+        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
+        _, segs = self.build(r, log)
+        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
+        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)
+
+    def test_away_carries_across_fast_segments(self):
+        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
+        r = Rec()
+        r.send(0.0, pose(-60, j1=10), rpc=0.03)
+        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
+        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
+        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
+        res, segs = self.build(r)
+        self.assertLess(segs[0]["away_max"][4], 3.1)
+        self.assertGreater(segs[1]["away_max"][4], 5.5)
+        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
+        r2 = Rec()                                          # j4 の指令が変われば引き継がない
+        r2.send(0.0, pose(-60), rpc=0.03)
+        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
+        r2.send(1.0, pose(-57), rpc=0.03)
+        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
+        res2, _ = self.build(r2)
+        self.assertEqual(res2["events"], [])
+
+
 RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")
 
 
@@ -242,6 +389,21 @@ class TestToday(unittest.TestCase):
             else:
                 self.assertGreater(n_ev, 0, stem)
 
+    def test_20261106_gamepad(self):
+        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
+        for stem in ("183926", "184452", "184650", "184906", "185554"):
+            p = RUNS / f"pi05_20261106_{stem}"
+            if not p.is_dir():
+                self.skipTest("11-06 の記録がない")
+            res, _ = d.summarize_run(p)
+            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
+            self.assertGreater(res["fast_segments"], 0, stem)
+            if stem == "185554":
+                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
+                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
+            else:
+                self.assertEqual(res["events"], [], stem)
+
 
 if __name__ == "__main__":
     unittest.main()

exec
/bin/zsh -lc 'cat docs/review_result_20261106_corr_v5_gamepad_codex.md; cat pi05/diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 77ms:
# codex レビュー結果：ゲームパッドの点検・corr_v5・corr3（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad.md（codex exec -s read-only）

**corr3 の学習・推論の向け先は整合しています。ただし、診断コードには必須修正が 2 点あります。** 今回の「4 本を採用・185554 を除外」という結果は再現しましたが、「手首の垂れを見落とさない」とはまだ断定できません。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| pad 送信の対応付け | **必須** | [_pad_send_for](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:127) は `ts + 0.2` 以前の最新送信を選ぶため、次の送信を拾えます。送信 A=1.00 秒、B=1.15 秒、rpc=1.00 秒で **B を選ぶ**ことを再現しました。初回は時刻差が最小の未使用送信に対応させ、再送は対応済み送信の残り回数として扱い、曖昧なら unknown にしてください。今回の 5 本では未来送信との対応はありませんでした。 |
| fast の区間をまたぐ垂れ | **必須** | [away の計算](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:222) が区間ごとにリセットされます。j4 の指令が同じでも、別関節の送信で区切られ、実測が `0→3°`、次区間で `3→6°` と動き続けると、両区間とも away=3°・hold_err=NaN となり、6° の垂れを見逃します。**関節ごとに、指令が変わらない間の最小誤差を引き継ぐ**必要があります。再送・爪操作でも同様です。 |
| 再送の数え方 | 据え置き | `n_used > resends` の条件は初回 1 回＋再送数を許すため、回数に off-by-one はありません。ただし上記の対応付けを誤ると回数管理も崩れます。 |
| stop の指令復元 | 据え置き／推奨 | 既知の halt_joint だけ measured_after_halt に替える処理は `_apply_halt` と一致します。一方、halt の検索は範囲内の最初の行を選び、消費済み管理がありません。最近傍・一対一対応にすることを推奨します。 |
| fast の通常動作の扱い | 据え置き | 指令へ単調に近づく動きを lag として数えない方針は妥当です。動き出す前の止まりを除く処理も意図と一致します。ただし、away は原因を区別しないため、4°超のオーバーシュートも「外れ」になります。 |
| 11-01 との互換性 | 据え置き：確認済み | 9 本すべて fast=0。既存 JSON にある全結果項目が一致しました。 |
| 11-06 の選別結果 | 据え置き：再現済み | 5 本とも pad_unknown=0。採用する 4 本は events なし。185554 は j4 の外れが 4 区間で、最大約 6.8°。ただし上記の検出漏れを直した後の再点検が必要です。 |
| corr3 の学習経路 | 据え置き：整合 | [モデル定義](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_lib.sh:306)から、v5 tar → Pod の v5 データ → `pi05_mecharm_trim_corr_v5_lora`／`local/..._trim2_corr_v5` へ一致。初期重みは corr2 を `/workspace/ckpt/corr_v2_1499` に取得し、その params を使用します。 |
| 出力・由来 | 据え置き：整合 | HF 出力は `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3`、KIOXIA は `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499`。由来の入力も corr2 の `..._v2/1499`、データも v5 です。既存 trim・corr・corr2 の分岐に動作変更は見つかりませんでした。 |
| 正規化と推論 assets | 据え置き：整合 | corr2 の v4b assets にある正規化ファイルの SHA256 は指定値と一致。v5 の repo_id が既定 asset_id となるため、保存先は `assets/local/..._trim2_corr_v5` で、corr3 推論のコピー元と一致します。[固定コミットの設定実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/config.py)、[保存実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/checkpoints.py) |
| Log22 の Pod 記録 | 据え置き：分離済み | 推論は `Log22/pod/lab_pod.json`、学習は `Log22/train/lab_pod.json` で衝突しません。推論側には 18:58:28 の終了記録があります。確認時点では train ディレクトリは未作成でした。 |
| データの構成 | 据え置き：確認済み | 統合版は 30 本・4301 コマ。旧 7 本の episode.npz はバイト一致。tar 内も 30 本で `._` はありません。 |
| 184650 の範囲超過 | **推奨：評価条件に明記** | 実データの j2 上限は trim2=31.37°、v5=42.45°。推論側は trim2＋10°なので上限約41.37°となり、**学習軌道の末端約1.08°はクランプされます**。追加自体を禁止する理由にはなりませんが、同じ軌道を完全再現できない条件として扱ってください。 |
| 4.8°／6.7° のコマ間移動 | 推奨 | 従来より大きい動きを教師にするため、出力の移動量や補正頻度が増える可能性があります。正規化の維持は妥当ですが、成功率だけでなくクランプ・移動量制限の発生数も比較すると判断できます。1コマ差に加え、学習が使うチャンク先頭からの差分も確認すると有用です。 |
| 学習テストの assets | 推奨 | [偽物チェックポイント](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_train_fakes.sh:72)は v3 assets 固定です。corr3 では v5 に切り替え、取得した assets 名まで検査すると学習→推論間の不整合を検出できます。 |

必須は **送信の対応付け**と**指令不変の関節で away を区間越しに保持する処理**です。この修正後に 5 本を再点検してから採用を確定するのが適切です。

読み取り専用の実ログテスト 2 件は成功しました。一時ファイルを生成するテスト群は未実行です。ファイル変更、実機・Pod・Hugging Face の操作は行っていません。
#!/usr/bin/env python3
"""diag_joint_hold.py — 実機推論の記録から, 関節ごとの「動いている途中の遅れ」と「止まったあとのずれ (垂れ)」を出す (2026-11-01 依頼 57).

読むだけ (runs/ には書かない). アームは使わない.

入力: runs/pi05_policy/{run_stem}/recording/states.csv と log.jsonl
  states.csv (依頼 46 のあとの記録) には send_angles 行 (送った時刻・目標・rpc_sec) と, 爪の rpc 行がある.
  依頼 46 より前の記録 (send_angles 行がない) は log.jsonl だけで見る (下の「log で見る」).

分け方 (states.csv で見る):
  - 指令 = 直前の send_angles の目標. 送ってから rpc が返るまで (daemon が着いたと判定するまで. 約 1.6 秒) は
    つながりがふさがり, 実測は残らない. rpc が返ったあとから次の送信までを「止まっているはずの区間」(hold) とする.
    爪の rpc (開け閉め. 指令は変わらない) でも区間を切る (爪の前・爪の後 を別に見る).
  - 区間の中のコマごと・関節ごとに, 前後 MOVE_WIN_SEC 以内の実測のふれが MOVE_EPS_DEG を超えていれば「動いている」.
    動いているコマの |実測 − 指令| を「動いている途中の遅れ」とする (着いたと判定されたあとも動いていた分も含む).
  - 動いていないコマの |実測 − 指令| を「止まったあとのずれ」とする. 関節ごとに, 区間の最後の動いていないコマの値を
    その区間のずれとし (区間の数で数える. 長く待った区間が重くならない), run ごとに中央値・90%・最大を出す.
    codex R1 必須: 動いているかは関節ごとに見る (j1 が動き続けていても, 止まった j4 のずれを見落とさない).
    その関節が区間の中で一度も止まっていなければ, その区間のずれは無し (NaN) とする.
  - 区間の最後の「止まりが続いたところ」(関節ごと. つながった動いていないコマ) の変化 (最後 − 最初) を
    「止まっている間の変化」(じわじわ垂れる) とし, 続いた秒も出す. MOVE_EPS_DEG に満たない速さで動き続けるものはここに出る.
  - 前後 0.3 秒の窓で動きが見えなかったコマを「動いていない」としている (区間の端・まばらな記録では 1 コマでも
    動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
  - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.

ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)

log で見る (依頼 46 より前の記録とも比べられる):
  - 補正の 1 刻み: measured_after − after (全 6 関節)
  - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)

Usage:
  python3 pi05/diag_joint_hold.py --run pi05_20261101_190347 --run pi05_20261101_194042
  python3 pi05/diag_joint_hold.py --run ... --json docs/diag_joint_hold_20261101.json
"""
import argparse
import csv
import json
import sys
from pathlib import Path

import numpy as np

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
RUNS_ROOT = DATA_ROOT / "runs" / "pi05_policy"
JOINTS = ["j0", "j1", "j2", "j3", "j4", "j5"]
MOVE_WIN_SEC = 0.3      # 前後これだけの間の実測のふれで「動いている」を見る
MOVE_EPS_DEG = 0.3      # ふれがこれを超えたら動いている (読みの刻みは約 0.09°)
LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いていないコマがこれだけ続いた区間で見る
# 指令が変わらないのに動いた・止まったあとも残ったずれがこれを超えた区間を「外れ」として数える (依頼 57).
# 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
# Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
EVENT_DEG = 4.0
# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
FAST_RPC_SEC = 0.5
PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限


def load_rows(path):
    rows = []
    with Path(path).open(newline="") as f:
        for r in csv.DictReader(f):
            try:
                r["_ts"] = float(r["ts"])
            except Exception:
                continue
            rows.append(r)
    rows.sort(key=lambda r: r["_ts"])
    return rows


def _q(r):
    try:
        return np.array([float(r[j]) for j in JOINTS])
    except Exception:
        return None


def _rpc_sec(r):
    for tok in (r.get("note") or "").split():
        if tok.startswith("rpc_sec="):
            try:
                return float(tok.split("=", 1)[1])
            except ValueError:
                return 0.0
    return 0.0


def pad_events(log):
    """log.jsonl の type=pad の行から, 送ったもの (move / home) と止めたもの (halt) を時刻の順に集める (依頼 97)."""
    sends, halts = [], []
    for r in log or ():
        if r.get("type") != "pad":
            continue
        if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None \
                and r.get("t_send") is not None:
            sends.append({"t": float(r["t_send"]), "cmd": np.asarray(r["command"][:6], float),
                          "joint": r.get("joint") if r.get("kind") == "move" else None,
                          "hold": bool(r.get("no_check")), "resends": len(r.get("resends") or []),
                          "corr": r.get("correction_id")})
        elif r.get("kind") == "halt" and r.get("t_halt") is not None:
            q = r.get("measured_after_halt")
            halts.append({"t": float(r["t_halt"]), "q": None if q is None else np.asarray(q[:6], float),
                          "joint": r.get("halt_joint"), "how": r.get("halt"), "corr": r.get("correction_id")})
    sends.sort(key=lambda x: x["t"]); halts.sort(key=lambda x: x["t"])
    return sends, halts


def _pad_send_for(sends, used, ts):
    """send_angle の rpc 行 (時刻 ts) に当たる pad の送信と, それが何回目か (0 = 初めて・1 以上 = 送り直し).
    codex R1 必須: 初めての送信は, まだ組にしていない送信のうち |t_send − ts| が PAD_MATCH_SEC 以内でいちばん近いもの.
    それがなければ, ts より前に組にした送信のうち送り直しの残りがあるいちばん新しいものの送り直しとする.
    どちらもなければ (None, 0). used = {送信の番号: 組にした回数}"""
    cand = [(abs(x["t"] - ts), k) for k, x in enumerate(sends) if k not in used and abs(x["t"] - ts) <= PAD_MATCH_SEC]
    if cand:
        k = min(cand)[1]
        used[k] = 1
        return sends[k], 0
    for k in sorted((k for k in used if sends[k]["t"] <= ts), key=lambda k: sends[k]["t"], reverse=True):
        if used[k] <= sends[k]["resends"]:
            used[k] += 1
            return sends[k], used[k] - 1
        break               # いちばん新しい送信の送り直しが尽きていれば, それより前のものにはしない
    return None, 0


def segments(rows, log=None):
    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), fast, skip, ts[], q[], stages[]}]
    log (log.jsonl の行) があれば, ゲームパッドの rpc 行 (send_angle / stop) の指令を pad の行から作る (依頼 97)."""
    sends, halts = pad_events(log)
    used_sends, used_halts = {}, set()
    segs, cur = [], None
    nan6 = np.full(6, np.nan)
    for r in rows:
        ev = r.get("event_type")
        if ev == "send_angles":
            q = _q(r)
            if q is None:
                continue
            rs = _rpc_sec(r)
            cur = {"kind": "send", "stage": r.get("stage"), "cmd": q, "t_cmd": r["_ts"],
                   "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC, "skip": None,
                   "ts": [], "q": [], "stages": []}
            segs.append(cur)
        elif ev == "rpc" and cur is not None:
            cmd = (r.get("note") or "").split()[0].replace("cmd=", "")
            rs = _rpc_sec(r)
            base = {"stage": r.get("stage"), "t_cmd": r["_ts"], "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC,
                    "skip": None, "ts": [], "q": [], "stages": []}
            if cmd == "send_angle":
                x, n_used = _pad_send_for(sends, used_sends, r["_ts"])
                if x is None:
                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
                else:
                    cur = dict(base, kind="pad_hold" if x["hold"] else "pad_step", cmd=x["cmd"].copy(),
                               skip=x["joint"] if x["hold"] else None, t_cmd=x["t"] if n_used == 0 else r["_ts"])
            elif cmd == "stop":
                # codex R1 推奨: いちばん近い・まだ組にしていない halt (一対一)
                hc = [(abs(x["t"] - r["_ts"]), k) for k, x in enumerate(halts)
                      if k not in used_halts and abs(x["t"] - r["_ts"]) <= PAD_MATCH_SEC]
                h = None
                if hc:
                    used_halts.add(min(hc)[1])
                    h = halts[min(hc)[1]]
                if h is None or h["q"] is None:
                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
                else:
                    c = np.asarray(cur["cmd"], float).copy()
                    if h["joint"] is not None and not np.isnan(c).any():
                        c[int(h["joint"])] = h["q"][int(h["joint"])]
                    else:
                        c = h["q"].copy()
                    cur = dict(base, kind="pad_stop", cmd=c)
            else:
                cur = dict(base, kind="grip_after", grip_cmd=cmd, cmd=cur["cmd"], t_cmd=cur["t_cmd"],
                           fast=cur.get("fast", False))
            segs.append(cur)
        elif ev == "actual" and cur is not None:
            q = _q(r)
            if q is None or r["_ts"] < cur["t0"] - 0.005:
                continue
            cur["ts"].append(r["_ts"]); cur["q"].append(q); cur["stages"].append(r.get("stage"))
    for s in segs:
        s["ts"] = np.array(s["ts"]); s["q"] = np.array(s["q"]).reshape(-1, 6)
    return segs


def moving_mask(ts, q, win=MOVE_WIN_SEC, eps=MOVE_EPS_DEG):
    """(n, 6) 関節ごとに, 前後 win 秒以内の実測のふれが eps を超えるか."""
    n = len(ts)
    mv = np.zeros((n, 6), dtype=bool)
    for i in range(n):
        m = np.abs(ts - ts[i]) <= win
        mv[i] = (q[m].max(axis=0) - q[m].min(axis=0)) > eps
    return mv


def analyze_segments(segs):
    """区間ごとの値 (関節ごと. 止まっていない関節は NaN).
    戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
    out = []
    nan6 = np.full(6, np.nan)
    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
    carry_cmd, carry_min = nan6.copy(), nan6.copy()
    for k, s in enumerate(segs):
        n = len(s["ts"])
        rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
               "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
               "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
        rec["away_max"] = np.zeros(6)
        rec["skip"] = s.get("skip")
        rec["fast"] = bool(s.get("fast"))
        if n and not np.isnan(s["cmd"]).any():
            err = s["q"] - s["cmd"]
            mv = moving_mask(s["ts"], s["q"])
            if s.get("fast"):
                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
                a = np.abs(err)
                same = np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0) & ~np.isnan(carry_min)
                start = np.where(same, np.fmin(carry_min, a[0]), a[0])
                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
                rec["away_max"] = np.max(a - run_min, axis=0)
                carry_cmd, carry_min = s["cmd"].copy(), run_min[-1].copy()
                if s.get("skip") is not None:
                    carry_min[int(s["skip"])] = np.nan
                lag_mv = np.zeros_like(mv)
            else:
                lag_mv = mv
                carry_cmd, carry_min = nan6.copy(), nan6.copy()
            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
            for i in range(6):
                if s.get("skip") is not None and i == int(s["skip"]):
                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                idx = np.flatnonzero(~mv[:, i])
                if s.get("fast") and mv[:, i].any():
                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                if not len(idx):
                    continue
                last = idx[-1]
                first = last
                while first - 1 >= 0 and not mv[first - 1, i]:   # 最後の止まりが続いたところの頭
                    first -= 1
                rec["hold_err"][i] = err[last, i]
                rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
            if s.get("skip") is not None:
                rec["lag_max"][int(s["skip"])] = 0.0
                rec["away_max"][int(s["skip"])] = 0.0
            ce = np.abs(err)
            if s.get("skip") is not None:
                ce[:, int(s["skip"])] = 0.0
            rec["coarse_max"] = ce.max(axis=0)
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        elif n:
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        out.append(rec)
    return out


def _has(v):
    return v is not None and not np.isnan(v)


def log_hold_errors(log):
    """log.jsonl だけで見る止まったあとのずれ. 戻り値: {"correction": [(t, err6)], "chunk": [(t, err6)]}"""
    out = {"correction": [], "chunk": []}
    last_cmd = None
    for r in log:
        if r.get("type") is None and "chunk" in r:
            ms = r.get("measured_state")
            if last_cmd is not None and ms is not None and r.get("state_source") is not None:
                out["chunk"].append((float(r["t"]), np.asarray(ms[:6], float) - last_cmd))
            if r.get("executed") and r.get("sent_targets"):
                last_cmd = np.asarray(r["sent_targets"][-1][:6], float)
        elif r.get("type") == "correction" and r.get("action") == "move" and r.get("sent"):
            if r.get("measured_after") is not None:
                out["correction"].append((float(r["t"]), np.asarray(r["measured_after"][:6], float)
                                          - np.asarray(r["after"][:6], float)))
            last_cmd = np.asarray(r["after"][:6], float)
        elif r.get("type") == "rewind":
            last_cmd = None
    return out


def _stats(vals):
    a = np.abs(np.asarray(vals, float))
    if not len(a):
        return None
    return {"n": int(len(a)), "median": round(float(np.median(a)), 2),
            "p90": round(float(np.percentile(a, 90)), 2), "max": round(float(a.max()), 2)}


def summarize_run(run_dir):
    run_dir = Path(run_dir)
    stem = run_dir.name
    trial = {}
    try:
        trial = json.loads((run_dir / f"{stem}.json").read_text())
    except Exception:
        pass
    log = []
    lp = run_dir / "log.jsonl"
    if lp.exists():
        for line in lp.read_text().splitlines():
            try:
                log.append(json.loads(line))
            except Exception:
                pass
    res = {"run": stem, "outcome": trial.get("outcome"), "corrections": trial.get("corrections")}
    rows = load_rows(run_dir / "recording" / "states.csv")
    segs = analyze_segments(segments(rows, log))
    res["has_send_rows"] = bool(segs)
    lh = log_hold_errors(log)
    res["log"] = {k: {j: _stats([e[i] for _, e in v]) for i, j in enumerate(JOINTS)} for k, v in lh.items()}
    if not segs:
        return res, segs
    work = [s for s in segs if s["stage"] != "home"]   # HOME は学習に入らない. 別に出す
    res["segments"] = len(segs)
    res["work_segments"] = len(work)
    res["coarse_max"] = {j: round(float(max(s.get("coarse_max", np.zeros(6))[i] for s in segs)), 2)
                         for i, j in enumerate(JOINTS)}
    # codex R1 必須: HOME の区間しかない記録でも止まらない (学習に入る区間がなければ, その値は 0 / None)
    res["lag_max"] = {j: round(float(max([s["lag_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}

    def holds(i, ss=work):
        return [float(s["hold_err"][i]) for s in ss if _has(s["hold_err"][i])]
    res["hold"] = {j: _stats(holds(i)) for i, j in enumerate(JOINTS)}
    res["hold_signed_median"] = ({j: round(float(np.median(holds(i))), 2) if holds(i) else None
                                  for i, j in enumerate(JOINTS)} if work else None)
    by = {}
    for s in work:
        key = "grip_after" if s["kind"] == "grip_after" else s["stage"]
        by.setdefault(key, []).append(s)

    def _mx(v):
        return round(float(max(np.abs(v))), 2) if v else None
    res["hold_by_context"] = {k: {"n": len(v), "j4_max": _mx(holds(4, v)),
                                  "j4_median": round(float(np.median(np.abs(holds(4, v)))), 2) if holds(4, v) else None,
                                  "j1_max": _mx(holds(1, v)), "j2_max": _mx(holds(2, v))}
                              for k, v in sorted(by.items())}
    # 学習に入る段 (HOME 以外) で, 止まったあとのずれ・動いている途中の遅れのどちらかが EVENT_DEG を超えた区間・関節の組
    ev = []
    for s in work:
        for i, j in enumerate(JOINTS):
            he = float(s["hold_err"][i]) if _has(s["hold_err"][i]) else None
            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG or s["away_max"][i] > EVENT_DEG:
                e = {"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
                     "joint": j, "cmd": round(float(s["cmd"][i]), 2),
                     "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
                     "hold_err": None if he is None else round(he, 2),
                     "lag_max": round(float(s["lag_max"][i]), 2)}
                if s["away_max"][i] > 0:
                    e["away_max"] = round(float(s["away_max"][i]), 2)      # 依頼 97 (送ったらすぐ返った区間だけ)
                ev.append(e)
    res["events"] = ev
    # 依頼 97: 送ったらすぐ返った区間の数・ゲームパッドの区間の数・組にできなかった rpc 行の数
    res["fast_segments"] = sum(1 for s in segs if s.get("fast"))
    res["pad_segments"] = {k: sum(1 for s in segs if s["kind"] == k) for k in ("pad_step", "pad_hold", "pad_stop",
                                                                             "pad_unknown")}
    res["away_max"] = {j: round(float(max([s["away_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}
    res["event_segments"] = len({e["seg"] for e in ev})   # codex R1・R2 推奨: 区間の数 (区間の番号で数える. 関節は重ねない)
    res["drift_max"] = {j: round(float(max([abs(s["drift"][i]) for s in work
                                            if _has(s["drift"][i]) and s["still_sec"][i] >= LONG_HOLD_SEC] or [0.0])), 2)
                        for i, j in enumerate(JOINTS)}
    # 爪の開け閉めの前後: 爪の rpc の直前の区間の最後の実測 → 爪のあとの区間の最初の実測
    grips = []
    for a, b in zip(segs, segs[1:]):
        if b["kind"] == "grip_after" and a["n"] and b["n"]:
            grips.append({"t": round(b["t0"], 2), "cmd": b["grip_cmd"], "stage": b["stage"],
                          "delta": [round(float(v), 2) for v in b["first_q"] - a["last_q"]],
                          # codex R2 必須: 爪のあとに止まらなかった関節は None (最初の実測のずれで埋めない. それは別に出す)
                          "err_after": [round(float(h), 2) if _has(h) else None for h in b["hold_err"]],
                          "err_first": [round(float(v), 2) for v in b["first_q"] - b["cmd"]]})
    res["grip"] = grips
    homes = [s for s in segs if s["stage"] == "home" and s["n"]]
    res["home"] = [{"t": round(s["t"], 2), "err_last": [round(float(v), 2) for v in s["last_q"] - s["cmd"]],
                    "lag_max": [round(float(v), 2) for v in s["lag_max"]],
                    "drift": [round(float(v), 2) if _has(v) else None for v in s["drift"]],
                    "still_sec": [round(float(v), 1) for v in s["still_sec"]]} for s in homes]
    return res, segs


def timeline(segs, t_start, joint=4, min_abs=0.0):
    """区間ごとの (経過秒, 段, 種類, 止まったあとのずれ, 動いている途中の遅れ, 止まっている間の変化) の一覧."""
    out = []
    for s in segs:
        he = round(float(s["hold_err"][joint]), 2) if _has(s["hold_err"][joint]) else None
        if (he is None or abs(he) < min_abs) and s["lag_max"][joint] < min_abs:
            continue
        out.append({"sec": round(s["t"] - t_start, 1), "stage": s["stage"], "kind": s["kind"],
                    "cmd": round(float(s["cmd"][joint]), 2), "hold_err": he,
                    "lag_max": round(float(s["lag_max"][joint]), 2),
                    "drift": round(float(s["drift"][joint]), 2) if _has(s["drift"][joint]) else None,
                    "still_sec": round(float(s["still_sec"][joint]), 1)})
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--runs-root", default=str(RUNS_ROOT))
    ap.add_argument("--run", action="append", required=True)
    ap.add_argument("--timeline", action="append", default=[], help="区間ごとの一覧を出す run (j4 と j1・j2)")
    ap.add_argument("--json", help="結果を JSON で書く (runs/ の外. 既存のファイルには書かない)")
    args = ap.parse_args()
    root = Path(args.runs_root)
    if str(root).startswith("/Volumes/KIOXIA") and not Path("/Volumes/KIOXIA").is_mount():
        sys.exit("[ABORT] KIOXIA not mounted: /Volumes/KIOXIA")
    out = {"params": {"move_win_sec": MOVE_WIN_SEC, "move_eps_deg": MOVE_EPS_DEG, "event_deg": EVENT_DEG,
                      "long_hold_sec": LONG_HOLD_SEC, "fast_rpc_sec": FAST_RPC_SEC}, "runs": {}, "timelines": {}}
    for stem in args.run:
        res, segs = summarize_run(root / stem)
        out["runs"][stem] = res
        print(f"## {stem}  outcome={res['outcome']} corrections={res['corrections']}")
        if not res["has_send_rows"]:
            print("   send_angles 行なし (依頼 46 より前). log で見る値だけ")
        else:
            print("   粗い最大   " + " ".join(f"{j}={v:5.1f}" for j, v in res["coarse_max"].items()))
            print("   動き途中   " + " ".join(f"{j}={v:5.1f}" for j, v in res["lag_max"].items()))
            def _h(k):          # codex R2 推奨: 止まった区間のない関節は「-」(0.0 と取り違えない)
                return " ".join(f"{j}={'-' if res['hold'][j] is None else format(res['hold'][j][k], '5.1f'):>5}"
                                for j in JOINTS)
            print("   止まり最大 " + _h("max"))
            print("   止まり90%  " + _h("p90"))
            print("   止まり中央 " + _h("median"))
            print("   止まり符号 " + " ".join(f"{j}={'-' if v is None else f'{v:+.1f}':>5}" for j, v in (res["hold_signed_median"] or {}).items()))
            print("   じわじわ   " + " ".join(f"{j}={v:5.1f}" for j, v in res["drift_max"].items()))
            if res["fast_segments"]:
                print("   向かう途中に離れた " + " ".join(f"{j}={v:5.1f}" for j, v in res["away_max"].items())
                      + f"  (すぐ返った区間 {res['fast_segments']}・ゲームパッド {res['pad_segments']})")
            print(f"   段ごと {res['hold_by_context']}")
            print(f"   外れ (>{EVENT_DEG:.0f}°) {res['event_segments']} 区間 ({len(res['events'])} 区間・関節の組)")
            for e in res["events"]:
                print(f"     {e['sec']:6.1f}秒 {e['stage']:7s} {e['kind']:10s} {e['joint']} 指令 {e['cmd']:7.2f} "
                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}"
                      + (f"  離れた {e['away_max']}" if "away_max" in e else ""))
            for gr in res["grip"]:
                print(f"   爪 {gr['cmd']} ({gr['stage']}) 前後の変化 {gr['delta']}  爪のあとのずれ {gr['err_after']}")
            for h in res["home"]:
                print(f"   HOME 最後のずれ {h['err_last']}  動き途中 {h['lag_max']}  止まり中の変化 {h['drift']}")
        for k in ("correction", "chunk"):
            st = res["log"][k]
            if st["j4"]:
                print(f"   log {k:10s} n={st['j4']['n']:2d} " + " ".join(
                    f"{j}={st[j]['max']:5.1f}" for j in JOINTS) + "  (最大)")
        if stem in args.timeline and segs:
            t0 = segs[0]["t"]
            tl = {j: timeline(segs, t0, joint=i) for i, j in ((1, "j1"), (2, "j2"), (4, "j4"))}
            out["timelines"][stem] = tl
            print("   区間ごと (j4): 秒 段 種類 指令 止まりのずれ 動き途中 止まり中の変化 止まった秒")
            for e in tl["j4"]:
                print(f"     {e['sec']:6.1f} {e['stage']:7s} {e['kind']:10s} {e['cmd']:7.2f} "
                      f"{'' if e['hold_err'] is None else e['hold_err']:>7} {e['lag_max']:6.2f} "
                      f"{'' if e['drift'] is None else e['drift']:>6} {e['still_sec']:5.1f}")
    if args.json:
        p = Path(args.json)
        if p.exists():
            sys.exit(f"[ABORT] 既にある: {p}")
        # 止まらなかった関節 (NaN) は null にする (JSON に NaN を書かない)
        p.write_text(json.dumps(out, indent=1, ensure_ascii=False, allow_nan=False,
                                default=lambda o: [None if isinstance(x, float) and np.isnan(x) else x for x in o.tolist()]))
        print(f"[JSON] {p}")


if __name__ == "__main__":
    main()

exec
/bin/zsh -lc 'cat pi05/test_diag_joint_hold.py' in /Users/shimazueiichi/r2r2r-scripts
exec
/bin/zsh -lc "head -65 docs/diag_joint_hold_20261106.txt; rg -n 'resends|t_send|t_halt' pi05/gamepad_teleop.py" in /Users/shimazueiichi/r2r2r-scripts
exec
/bin/zsh -lc 'cat docs/review_result_20261106_corr_v5_gamepad_codex.md' in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 330ms:
# codex レビュー結果：ゲームパッドの点検・corr_v5・corr3（2026-11-06 依頼 97）

依頼文: docs/review_request_20261106_corr_v5_gamepad.md（codex exec -s read-only）

**corr3 の学習・推論の向け先は整合しています。ただし、診断コードには必須修正が 2 点あります。** 今回の「4 本を採用・185554 を除外」という結果は再現しましたが、「手首の垂れを見落とさない」とはまだ断定できません。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| pad 送信の対応付け | **必須** | [_pad_send_for](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:127) は `ts + 0.2` 以前の最新送信を選ぶため、次の送信を拾えます。送信 A=1.00 秒、B=1.15 秒、rpc=1.00 秒で **B を選ぶ**ことを再現しました。初回は時刻差が最小の未使用送信に対応させ、再送は対応済み送信の残り回数として扱い、曖昧なら unknown にしてください。今回の 5 本では未来送信との対応はありませんでした。 |
| fast の区間をまたぐ垂れ | **必須** | [away の計算](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:222) が区間ごとにリセットされます。j4 の指令が同じでも、別関節の送信で区切られ、実測が `0→3°`、次区間で `3→6°` と動き続けると、両区間とも away=3°・hold_err=NaN となり、6° の垂れを見逃します。**関節ごとに、指令が変わらない間の最小誤差を引き継ぐ**必要があります。再送・爪操作でも同様です。 |
| 再送の数え方 | 据え置き | `n_used > resends` の条件は初回 1 回＋再送数を許すため、回数に off-by-one はありません。ただし上記の対応付けを誤ると回数管理も崩れます。 |
| stop の指令復元 | 据え置き／推奨 | 既知の halt_joint だけ measured_after_halt に替える処理は `_apply_halt` と一致します。一方、halt の検索は範囲内の最初の行を選び、消費済み管理がありません。最近傍・一対一対応にすることを推奨します。 |
| fast の通常動作の扱い | 据え置き | 指令へ単調に近づく動きを lag として数えない方針は妥当です。動き出す前の止まりを除く処理も意図と一致します。ただし、away は原因を区別しないため、4°超のオーバーシュートも「外れ」になります。 |
| 11-01 との互換性 | 据え置き：確認済み | 9 本すべて fast=0。既存 JSON にある全結果項目が一致しました。 |
| 11-06 の選別結果 | 据え置き：再現済み | 5 本とも pad_unknown=0。採用する 4 本は events なし。185554 は j4 の外れが 4 区間で、最大約 6.8°。ただし上記の検出漏れを直した後の再点検が必要です。 |
| corr3 の学習経路 | 据え置き：整合 | [モデル定義](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_lib.sh:306)から、v5 tar → Pod の v5 データ → `pi05_mecharm_trim_corr_v5_lora`／`local/..._trim2_corr_v5` へ一致。初期重みは corr2 を `/workspace/ckpt/corr_v2_1499` に取得し、その params を使用します。 |
| 出力・由来 | 据え置き：整合 | HF 出力は `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3`、KIOXIA は `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499`。由来の入力も corr2 の `..._v2/1499`、データも v5 です。既存 trim・corr・corr2 の分岐に動作変更は見つかりませんでした。 |
| 正規化と推論 assets | 据え置き：整合 | corr2 の v4b assets にある正規化ファイルの SHA256 は指定値と一致。v5 の repo_id が既定 asset_id となるため、保存先は `assets/local/..._trim2_corr_v5` で、corr3 推論のコピー元と一致します。[固定コミットの設定実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/config.py)、[保存実装](https://raw.githubusercontent.com/Physical-Intelligence/openpi/215abfb/src/openpi/training/checkpoints.py) |
| Log22 の Pod 記録 | 据え置き：分離済み | 推論は `Log22/pod/lab_pod.json`、学習は `Log22/train/lab_pod.json` で衝突しません。推論側には 18:58:28 の終了記録があります。確認時点では train ディレクトリは未作成でした。 |
| データの構成 | 据え置き：確認済み | 統合版は 30 本・4301 コマ。旧 7 本の episode.npz はバイト一致。tar 内も 30 本で `._` はありません。 |
| 184650 の範囲超過 | **推奨：評価条件に明記** | 実データの j2 上限は trim2=31.37°、v5=42.45°。推論側は trim2＋10°なので上限約41.37°となり、**学習軌道の末端約1.08°はクランプされます**。追加自体を禁止する理由にはなりませんが、同じ軌道を完全再現できない条件として扱ってください。 |
| 4.8°／6.7° のコマ間移動 | 推奨 | 従来より大きい動きを教師にするため、出力の移動量や補正頻度が増える可能性があります。正規化の維持は妥当ですが、成功率だけでなくクランプ・移動量制限の発生数も比較すると判断できます。1コマ差に加え、学習が使うチャンク先頭からの差分も確認すると有用です。 |
| 学習テストの assets | 推奨 | [偽物チェックポイント](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/lab_train_fakes.sh:72)は v3 assets 固定です。corr3 では v5 に切り替え、取得した assets 名まで検査すると学習→推論間の不整合を検出できます。 |

必須は **送信の対応付け**と**指令不変の関節で away を区間越しに保持する処理**です。この修正後に 5 本を再点検してから採用を確定するのが適切です。

読み取り専用の実ログテスト 2 件は成功しました。一時ファイルを生成するテスト群は未実行です。ファイル変更、実機・Pod・Hugging Face の操作は行っていません。

 succeeded in 433ms:
#!/usr/bin/env python3
"""test_diag_joint_hold.py — diag_joint_hold.py (依頼 57) の確かめ.

  - 動いている途中の遅れと, 止まったあとのずれを分ける (rpc が返ったあとの実測だけを使う)
  - 指令が変わらないのに動いた区間を「外れ」として数える. HOME は数えない
  - 爪の rpc で区間を切る
  - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
  - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
"""
import csv
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

import numpy as np

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))

import diag_joint_hold as d  # noqa: E402

COLS = ["ts", "event_type", "stage", "j0", "j1", "j2", "j3", "j4", "j5", "speed", "gripper_state",
        "ts_send", "ts_arrival", "attempt", "arrived", "note"]


class Rec:
    def __init__(self):
        self.rows = []

    def send(self, t, q, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})

    def rpc(self, t, cmd, stage="correct", rpc=0.03):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})

    def grip(self, t, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})

    def actual(self, t0, t1, q, stage="model", dt=0.1):
        t = t0
        while t < t1 - 1e-9:
            qq = q(t) if callable(q) else q
            self.rows.append({"ts": round(t, 3), "event_type": "actual", "stage": stage, **_j(qq)})
            t += dt

    def write(self, run_dir, log=()):
        rec = run_dir / "recording"
        rec.mkdir(parents=True)
        with (rec / "states.csv").open("w", newline="") as f:
            w = csv.DictWriter(f, fieldnames=COLS)
            w.writeheader()
            for r in self.rows:
                w.writerow({k: r.get(k, "") for k in COLS})
        (run_dir / "log.jsonl").write_text("".join(json.dumps(x) + "\n" for x in log))
        (run_dir / f"{run_dir.name}.json").write_text(json.dumps({"outcome": "success", "corrections": 1}))


def _j(q):
    return {f"j{i}": f"{v:.3f}" for i, v in enumerate(q)}


def pose(j4, j1=10.0):
    return [0.0, j1, 0.0, 0.0, j4, -30.0]


class TestSegments(unittest.TestCase):
    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_x"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_hold_vs_lag_and_events(self):
        r = Rec()
        # 1. 送って rpc が返ったあと, 指令から 1° ずれて止まる (ふつうの止まり)
        r.send(0.0, pose(-50))
        r.actual(1.5, 4.0, pose(-49))
        # 2. 送ったあと 2 秒かけて -60 → -55 へ動き (指令は -60), そのまま止まる (外れ: 指令なしに +5°)
        r.send(4.0, pose(-60))
        r.actual(5.5, 7.5, lambda t: pose(-60 + 5 * min(1.0, (t - 5.5) / 1.0)))
        r.actual(7.5, 9.0, pose(-55))
        # 3. HOME で戻らない (数えない)
        r.send(9.0, pose(0), stage="home")
        r.actual(10.5, 12.0, pose(40), stage="home")
        res, segs = self.build(r)
        self.assertEqual(len(segs), 3)
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 5.0, places=1)       # 止まったあとのずれ (2.)
        self.assertAlmostEqual(res["lag_max"]["j4"], 5.0, places=1)           # 動いている途中 (2. の頭は 0° ずれ… 最後の動きで 5°)
        self.assertEqual(res["hold_by_context"]["model"]["n"], 2)
        ev = res["events"]
        self.assertEqual([(e["sec"], e["joint"]) for e in ev], [(4.0, "j4")])  # HOME は数えない
        self.assertAlmostEqual(ev[0]["hold_err"], 5.0, places=1)
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 40.0, places=1)
        self.assertAlmostEqual(res["coarse_max"]["j4"], 40.0, places=1)       # 粗い値は HOME も含む
        self.assertAlmostEqual(res["hold_signed_median"]["j1"], 0.0, places=2)

    def test_no_samples_during_rpc_and_still_threshold(self):
        r = Rec()
        r.send(0.0, pose(-50), rpc=1.6)
        r.actual(0.0, 1.5, pose(-30))            # rpc の途中の値 (使わない)
        r.actual(1.6, 3.0, pose(-50.2))
        res, segs = self.build(r)
        self.assertEqual(segs[0]["n"], 14)
        self.assertEqual(res["events"], [])
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 0.2, places=2)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_grip_splits_segment(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 2.5, pose(-50))
        r.grip(2.5)
        r.actual(4.0, 5.0, pose(-46))             # 爪のあとで 4° 動いていた (外れの手前)
        res, segs = self.build(r)
        self.assertEqual([s["kind"] for s in segs], ["send", "grip_after"])
        self.assertEqual(res["grip"][0]["delta"][4], 4.0)
        self.assertAlmostEqual(res["hold_by_context"]["grip_after"]["j4_max"], 4.0, places=2)
        self.assertEqual(res["events"], [])        # 4.0 は外れの目安 (4° を超える) に入らない

    def test_drift_while_still(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 6.5, lambda t: pose(-50 + 0.2 * (t - 1.5)))   # 0.2°/秒 でじわじわ (動いているとはしない)
        res, _ = self.build(r)
        self.assertAlmostEqual(res["drift_max"]["j4"], 1.0, delta=0.05)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_joint_wise_still(self):
        """codex R1 必須: j1 が動き続けていても, 止まった j4 の 5° のずれを見落とさない."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))   # j1 は 2°/秒 で動き続ける. j4 は -55 で止まり
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][1]))                    # j1 は一度も止まっていない
        self.assertAlmostEqual(segs[0]["hold_err"][4], 5.0, places=2)
        self.assertEqual([e["joint"] for e in res["events"]], ["j1", "j4"])
        self.assertEqual(res["event_segments"], 1)                           # 区間の数は 1
        self.assertIsNone(res["hold"]["j1"])

    def test_grip_after_not_still_is_none(self):
        """codex R2 必須: 爪のあとに止まらなかった関節のずれは None (最初の実測のずれで埋めない)."""
        r = Rec()
        r.send(0.0, pose(-50, j1=10))
        r.actual(1.5, 2.5, pose(-50, j1=10))
        r.grip(2.5)
        r.actual(4.0, 6.0, lambda t: pose(-50, j1=10 + 2.0 * (t - 4.0)))
        res, _ = self.build(r)
        g = res["grip"][0]
        self.assertIsNone(g["err_after"][1])
        self.assertEqual(g["err_after"][4], 0.0)
        self.assertEqual(g["err_first"][1], 0.0)

    def test_event_segments_by_index(self):
        """codex R2 推奨: 秒が近い別の区間も別に数える."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.01)
        r.actual(0.01, 0.03, pose(-50))
        r.send(0.04, pose(-60), rpc=0.01)
        r.actual(0.05, 1.0, pose(-50))
        res, _ = self.build(r)
        self.assertEqual(res["event_segments"], 2)

    def test_home_only(self):
        """codex R1 必須: HOME の区間しかない記録でも止まらない."""
        r = Rec()
        r.send(0.0, pose(0), stage="home")
        r.actual(1.5, 3.0, pose(1), stage="home")
        res, _ = self.build(r)
        self.assertEqual(res["work_segments"], 0)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertIsNone(res["hold"]["j4"])
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 1.0, places=2)

    def test_drift_uses_last_still_run(self):
        """codex R1 推奨: 止まり中の変化は最後の止まりが続いたところだけ (途中で動いた分を入れない)."""
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 3.0, pose(-50))
        r.actual(3.0, 3.5, lambda t: pose(-50 + 10 * (t - 3.0)))           # 途中で 5° 動く
        r.actual(3.5, 6.0, pose(-45))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["drift"][4], 0.0, places=2)
        self.assertGreater(segs[0]["still_sec"][4], 1.5)
        self.assertLess(segs[0]["still_sec"][4], 2.5)

    def test_json_has_no_nan(self):
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))
        td = Path(tempfile.mkdtemp())
        r.write(td / "pi05_y")
        out = td / "o.json"
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--timeline", "pi05_y",
                                     "--json", str(out)]), mock.patch("sys.stdout"):
            d.main()
        self.assertNotIn("NaN", out.read_text())
        json.loads(out.read_text())
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--json", str(out)]), \
                mock.patch("sys.stdout"), self.assertRaises(SystemExit):
            d.main()                                                          # 既にあるファイルには書かない

    def test_log_hold_errors(self):
        log = [
            {"chunk": 0, "t": 1.0, "executed": True, "state_source": "measured",
             "measured_state": pose(-1) + [0.0], "sent_targets": [pose(-40)]},
            {"chunk": 1, "t": 2.0, "executed": False, "state_source": "target",
             "measured_state": pose(-37) + [0.0], "sent_targets": [pose(-45)]},
            {"type": "correction", "action": "move", "sent": True, "t": 3.0,
             "after": pose(-45), "measured_after": pose(-30)},
            {"type": "rewind", "t": 4.0},
            {"chunk": 2, "t": 5.0, "executed": True, "state_source": "measured", "measured_state": pose(9) + [0.0]},
        ]
        out = d.log_hold_errors(log)
        self.assertEqual([round(float(e[4]), 2) for _, e in out["chunk"]], [3.0])     # 実行していないチャンクの目標は使わない. 巻き戻しのあとは比べない
        self.assertEqual([round(float(e[4]), 2) for _, e in out["correction"]], [15.0])

    def test_old_record_without_send_rows(self):
        r = Rec()
        r.actual(0.0, 2.0, pose(-50))
        res, segs = self.build(r)
        self.assertFalse(res["has_send_rows"])
        self.assertEqual(segs, [])


def pad_move(t, command, joint, hold=False, resends=(), cid=1):
    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
         "joint": joint, "resends": list(resends)}
    if hold:
        r["no_check"] = True
    return r


def pad_halt(t, q, joint, cid=1):
    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
            "halt_joint": joint, "halt": "stop"}


class TestPadAndFast(unittest.TestCase):
    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""

    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_p"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_fast_approach_is_not_lag(self):
        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
        r.actual(2.3, 3.5, pose(-59.5))
        res, segs = self.build(r)
        self.assertEqual(res["fast_segments"], 1)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
        self.assertLess(res["away_max"]["j4"], 0.2)

    def test_fast_hold_ignores_still_before_moving(self):
        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.8, pose(-50))
        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
        self.assertEqual(res["events"], [])

    def test_fast_sag_after_arrival_is_event(self):
        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 1.0, pose(-60))
        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
        r.actual(1.6, 3.0, pose(-54))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)

    def test_pad_step_hold_stop(self):
        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
        start = pose(-60, j1=40)
        step = pose(-60, j1=40); step[0] = 1.0
        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
        r = Rec()
        r.send(0.0, start, stage="model", rpc=0.03)
        r.actual(0.03, 1.0, start, stage="model")
        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
        r.actual(2.03, 3.0, step, stage="correct")
        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)
        r.actual(3.03, 3.5, lambda t: [1.0, 40 - 20 * (t - 3.03), 0, 0, -60, -30], stage="correct")
        r.rpc(3.5, "stop")
        r.actual(3.55, 5.0, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
        r.rpc(5.0, "send_angle")                                               # 組にできない (pad の行がない… 送り直しの数を越える)
        r.actual(5.03, 5.5, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
        log = [pad_move(2.0, step, 0), pad_move(3.0, far, 1, hold=True), pad_halt(3.5, [1.0, 30.6, 0, 0, -60.2, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_step", "pad_hold", "pad_stop", "pad_unknown"])
        self.assertEqual(res["pad_segments"], {"pad_step": 1, "pad_hold": 1, "pad_stop": 1, "pad_unknown": 1})
        np.testing.assert_allclose(segs[1]["cmd"], step)
        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
        self.assertEqual(segs[2]["lag_max"][1], 0.0)
        self.assertEqual(segs[2]["skip"], 1)
        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
        self.assertEqual(res["events"], [])

    def test_pad_resend_and_uncommanded_wrist(self):
        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
        start = pose(-60, j1=50)
        far = pose(-60, j1=35)
        r = Rec()
        r.send(0.0, start, rpc=0.03)
        r.actual(0.03, 1.0, start)
        r.rpc(2.0, "send_angle")
        r.rpc(2.3, "send_angle")                                               # 送り直し
        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
        r.rpc(3.0, "stop")
        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)

    def test_pad_send_not_matched_to_next(self):
        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "send_angle")
        r.actual(1.03, 1.14, a_cmd, stage="correct")
        r.rpc(1.16, "send_angle")
        r.actual(1.19, 2.0, b_cmd, stage="correct")
        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
        _, segs = self.build(r, log)
        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)

    def test_away_carries_across_fast_segments(self):
        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10), rpc=0.03)
        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
        res, segs = self.build(r)
        self.assertLess(segs[0]["away_max"][4], 3.1)
        self.assertGreater(segs[1]["away_max"][4], 5.5)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        r2 = Rec()                                          # j4 の指令が変われば引き継がない
        r2.send(0.0, pose(-60), rpc=0.03)
        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
        r2.send(1.0, pose(-57), rpc=0.03)
        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
        res2, _ = self.build(r2)
        self.assertEqual(res2["events"], [])


RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")


@unittest.skipUnless(RUNS.is_dir(), "KIOXIA がない")
class TestToday(unittest.TestCase):
    """依頼文の粗い値 (j4 の「直前に送った指令と実測の差」の最大) と同じになり, 入れる 3 本には外れがない."""
    COARSE = {"190347": 3.5, "190854": 2.5, "192101": 1.8, "192418": 10.3, "192758": 19.5,
              "193041": 18.4, "193411": 8.0, "194042": 59.5}

    def test_coarse_and_events(self):
        for stem, v in self.COARSE.items():
            res, _ = d.summarize_run(RUNS / f"pi05_20261101_{stem}")
            self.assertAlmostEqual(res["coarse_max"]["j4"], v, delta=0.051, msg=stem)
            n_ev = len([e for e in res["events"] if e["joint"] == "j4"])
            if stem in ("190347", "190854", "192101"):
                self.assertEqual(n_ev, 0, stem)
            else:
                self.assertGreater(n_ev, 0, stem)

    def test_20261106_gamepad(self):
        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
        for stem in ("183926", "184452", "184650", "184906", "185554"):
            p = RUNS / f"pi05_20261106_{stem}"
            if not p.is_dir():
                self.skipTest("11-06 の記録がない")
            res, _ = d.summarize_run(p)
            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
            self.assertGreater(res["fast_segments"], 0, stem)
            if stem == "185554":
                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
            else:
                self.assertEqual(res["events"], [], stem)


if __name__ == "__main__":
    unittest.main()

 exited 2 in 472ms:
## pi05_20261106_183926  outcome=success corrections=2
   粗い最大   j0=  8.0 j1=  9.1 j2=  5.5 j3=  2.0 j4=  9.6 j5=  1.8
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.7 j1=  2.1 j2=  2.1 j3=  1.1 j4=  2.2 j5=  0.7
   止まり90%  j0=  0.5 j1=  1.9 j2=  1.6 j3=  0.6 j4=  1.8 j5=  0.6
   止まり中央 j0=  0.1 j1=  0.9 j2=  0.9 j3=  0.3 j4=  0.8 j5=  0.3
   止まり符号 j0= +0.0 j1= +0.9 j2= +0.9 j3= +0.1 j4= +0.2 j5= -0.1
   じわじわ   j0=  0.2 j1=  0.2 j2=  0.2 j3=  0.3 j4=  0.9 j5=  0.3
   向かう途中に離れた j0=  0.3 j1=  1.1 j2=  0.8 j3=  0.8 j4=  1.2 j5=  0.2  (すぐ返った区間 72・ゲームパッド {'pad_step': 9, 'pad_hold': 4, 'pad_stop': 4, 'pad_unknown': 0})
   段ごと {'correct': {'n': 17, 'j4_max': 1.67, 'j4_median': 0.18, 'j1_max': 2.0, 'j2_max': 1.3}, 'grip_after': {'n': 2, 'j4_max': 2.21, 'j4_median': 1.34, 'j1_max': 1.07, 'j2_max': 1.43}, 'model': {'n': 51, 'j4_max': 2.03, 'j4_median': 1.03, 'j1_max': 2.13, 'j2_max': 2.15}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.45, 0.54, 0.68, 0.53, -2.21, -0.25]
   爪 set_gripper (model) 前後の変化 [-0.09, 0.0, 0.0, 0.09, 0.0, -0.08]  爪のあとのずれ [0.0, 1.07, 1.43, -0.23, 0.47, -0.26]
   HOME 最後のずれ [-0.14, 1.48, 2.15, 0.3, -0.65, 0.08]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
   HOME 最後のずれ [0.0, 1.14, 1.4, -0.43, -0.43, -0.49]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, 0.0, None, 0.18]
   log chunk      n=16 j0=  2.1 j1= 31.5 j2=  2.1 j3=  0.9 j4= 16.6 j5=  0.7  (最大)
## pi05_20261106_184452  outcome=success corrections=2
   粗い最大   j0= 25.3 j1= 28.1 j2=  8.2 j3=  2.0 j4= 30.7 j5=  1.7
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  1.0 j1=  2.6 j2=  1.9 j3=  1.7 j4=  3.3 j5=  1.2
   止まり90%  j0=  0.8 j1=  1.3 j2=  1.2 j3=  1.1 j4=  2.0 j5=  0.6
   止まり中央 j0=  0.2 j1=  0.9 j2=  0.4 j3=  0.3 j4=  0.7 j5=  0.3
   止まり符号 j0= -0.1 j1= +0.9 j2= +0.4 j3= +0.1 j4= +0.6 j5= -0.1
   じわじわ   j0=  0.3 j1=  0.1 j2=  0.0 j3=  0.3 j4=  0.2 j5=  0.9
   向かう途中に離れた j0=  1.2 j1=  1.8 j2=  1.2 j3=  1.5 j4=  3.8 j5=  0.2  (すぐ返った区間 61・ゲームパッド {'pad_step': 11, 'pad_hold': 5, 'pad_stop': 4, 'pad_unknown': 0})
   段ごと {'correct': {'n': 20, 'j4_max': 2.99, 'j4_median': 0.62, 'j1_max': 1.84, 'j2_max': 1.1}, 'grip_after': {'n': 2, 'j4_max': 2.04, 'j4_median': 1.27, 'j1_max': 1.19, 'j2_max': 1.18}, 'model': {'n': 37, 'j4_max': 3.34, 'j4_median': 0.77, 'j1_max': 2.6, 'j2_max': 1.95}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.6, 0.84, 0.93, 0.41, -2.04, 0.13]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, -1.06, 0.0]  爪のあとのずれ [-0.51, 1.19, 1.18, -0.2, 0.49, 0.27]
   HOME 最後のずれ [0.23, 1.77, 1.02, 0.52, -0.84, -0.45]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.09]
   HOME 最後のずれ [-0.26, 1.4, 1.75, 0.7, -0.79, -1.02]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, -0.53, None, 0.0]
   log chunk      n=12 j0=  2.7 j1= 24.1 j2=  3.0 j3=  0.8 j4=  4.3 j5=  1.3  (最大)
## pi05_20261106_184650  outcome=success corrections=2
   粗い最大   j0= 22.0 j1= 24.3 j2=  7.2 j3=  1.9 j4= 30.5 j5=  1.9
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  0.8 j1=  2.6 j2=  1.9 j3=  0.9 j4=  3.0 j5=  0.6
   止まり90%  j0=  0.6 j1=  2.0 j2=  1.5 j3=  0.5 j4=  2.5 j5=  0.4
   止まり中央 j0=  0.1 j1=  1.1 j2=  1.0 j3=  0.3 j4=  0.8 j5=  0.2
   止まり符号 j0= -0.1 j1= +0.8 j2= +1.0 j3= -0.1 j4= +0.0 j5= +0.0
   じわじわ   j0=  0.3 j1=  0.1 j2=  0.5 j3=  0.2 j4=  0.3 j5=  0.2
   向かう途中に離れた j0=  0.4 j1=  2.6 j2=  1.3 j3=  1.1 j4=  2.7 j5=  0.2  (すぐ返った区間 59・ゲームパッド {'pad_step': 1, 'pad_hold': 6, 'pad_stop': 3, 'pad_unknown': 0})
   段ごと {'correct': {'n': 10, 'j4_max': 2.99, 'j4_median': 0.27, 'j1_max': 2.63, 'j2_max': 1.85}, 'grip_after': {'n': 3, 'j4_max': 2.84, 'j4_median': 2.11, 'j1_max': 1.49, 'j2_max': 1.29}, 'model': {'n': 43, 'j4_max': 2.8, 'j4_median': 0.75, 'j1_max': 2.16, 'j2_max': 1.82}, 'wait': {'n': 1, 'j4_max': 2.11, 'j4_median': 2.11, 'j1_max': 1.49, 'j2_max': 0.44}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.18, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.7, 0.62, 1.07, 0.38, -2.84, -0.31]
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [0.08, -1.49, 0.44, -0.26, -2.11, 0.0]
   爪 set_gripper (model) 前後の変化 [0.35, 0.09, -0.18, -0.27, 0.0, 0.0]  爪のあとのずれ [0.12, 1.21, 1.29, -0.06, 0.72, 0.33]
   HOME 最後のずれ [0.01, 1.79, 1.08, 0.66, -0.82, -0.57]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, 0.0, 0.0, 0.0, 0.18, 0.0]
   HOME 最後のずれ [-0.17, 1.49, 1.75, 0.52, -0.87, -0.93]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, None, None, None]
   log chunk      n=14 j0=  1.0 j1= 42.1 j2= 17.1 j3=  0.7 j4=  2.8 j5=  0.6  (最大)
## pi05_20261106_184906  outcome=success corrections=2
   粗い最大   j0= 39.7 j1= 47.5 j2=  7.1 j3=  2.5 j4= 55.9 j5=  1.7
   動き途中   j0=  0.0 j1=  0.0 j2=  0.0 j3=  0.0 j4=  0.0 j5=  0.0
   止まり最大 j0=  1.0 j1=  3.0 j2=  2.8 j3=  0.9 j4=  3.1 j5=  1.4
   止まり90%  j0=  0.5 j1=  1.5 j2=  1.7 j3=  0.6 j4=  2.7 j5=  0.6
   止まり中央 j0=  0.1 j1=  1.0 j2=  0.8 j3=  0.3 j4=  1.4 j5=  0.2
   止まり符号 j0= -0.1 j1= +1.0 j2= +0.8 j3= +0.3 j4= -1.4 j5= +0.0
   じわじわ   j0=  0.3 j1=  0.2 j2=  0.3 j3=  0.4 j4=  1.4 j5=  1.0
   向かう途中に離れた j0=  0.3 j1=  1.0 j2=  0.8 j3=  0.9 j4=  2.4 j5=  0.0  (すぐ返った区間 53・ゲームパッド {'pad_step': 5, 'pad_hold': 3, 'pad_stop': 2, 'pad_unknown': 0})
   段ごと {'correct': {'n': 10, 'j4_max': 2.72, 'j4_median': 2.72, 'j1_max': 1.15, 'j2_max': 1.64}, 'grip_after': {'n': 3, 'j4_max': 3.15, 'j4_median': 1.59, 'j1_max': 1.13, 'j2_max': 1.54}, 'model': {'n': 37, 'j4_max': 2.67, 'j4_median': 0.87, 'j1_max': 2.99, 'j2_max': 2.83}, 'wait': {'n': 1, 'j4_max': 1.49, 'j4_median': 1.49, 'j1_max': 0.0, 'j2_max': 0.79}}
   外れ (>4°) 0 区間 (0 区間・関節の組)
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [-0.49, -0.06, 1.54, 0.8, -3.15, -0.02]
   爪 set_gripper_value (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  爪のあとのずれ [0.0, 0.0, 0.79, 0.27, -1.49, -0.18]
   爪 set_gripper (model) 前後の変化 [0.0, 0.0, 0.0, 0.0, 0.0, -0.09]  爪のあとのずれ [-0.12, 1.13, 1.41, -0.2, -1.59, 0.29]
   HOME 最後のずれ [0.28, 2.04, 1.37, -0.18, -1.3, 0.3]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [0.0, -0.08, 0.0, 0.0, 0.35, 0.0]
   HOME 最後のずれ [0.0, 1.49, 1.75, 1.05, -1.05, -0.58]  動き途中 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  止まり中の変化 [None, None, None, None, None, 0.09]
rg: pi05/gamepad_teleop.py: IO error for operation on pi05/gamepad_teleop.py: No such file or directory (os error 2)

exec
/bin/zsh -lc "rg --files -g '*gamepad*' -g '*diag_joint_hold*' -g 'AGENTS.md'; nl -ba pi05/diag_joint_hold.py | sed -n '218,282p'; head -75 pi05/diag_joint_hold.py" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 158ms:
AGENTS.md
pi05/test_diag_joint_hold.py
pi05/diag_joint_hold.py
tools/gamepad_check.py
tools/test_gamepad_teleop.py
tools/gamepad_teleop.py
docs/diag_joint_hold_20261101.json
docs/review_result_20261103_gamepad_hold.md
docs/diag_joint_hold_20261106.txt
docs/diag_joint_hold_20261101.txt
docs/review_request_20261106_corr_v5_gamepad.md
docs/diag_joint_hold_20261106.json
docs/review_result_20261106_corr_v5_gamepad_codex.md
docs/review_request_20261106_corr_v5_gamepad_r2.md
docs/review_request_20261103_gamepad_hold.md
   218	    """区間ごとの値 (関節ごと. 止まっていない関節は NaN).
   219	    戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
   220	    out = []
   221	    nan6 = np.full(6, np.nan)
   222	    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
   223	    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
   224	    carry_cmd, carry_min = nan6.copy(), nan6.copy()
   225	    for k, s in enumerate(segs):
   226	        n = len(s["ts"])
   227	        rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
   228	               "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
   229	               "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
   230	        rec["away_max"] = np.zeros(6)
   231	        rec["skip"] = s.get("skip")
   232	        rec["fast"] = bool(s.get("fast"))
   233	        if n and not np.isnan(s["cmd"]).any():
   234	            err = s["q"] - s["cmd"]
   235	            mv = moving_mask(s["ts"], s["q"])
   236	            if s.get("fast"):
   237	                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
   238	                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
   239	                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
   240	                a = np.abs(err)
   241	                same = np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0) & ~np.isnan(carry_min)
   242	                start = np.where(same, np.fmin(carry_min, a[0]), a[0])
   243	                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
   244	                rec["away_max"] = np.max(a - run_min, axis=0)
   245	                carry_cmd, carry_min = s["cmd"].copy(), run_min[-1].copy()
   246	                if s.get("skip") is not None:
   247	                    carry_min[int(s["skip"])] = np.nan
   248	                lag_mv = np.zeros_like(mv)
   249	            else:
   250	                lag_mv = mv
   251	                carry_cmd, carry_min = nan6.copy(), nan6.copy()
   252	            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
   253	            for i in range(6):
   254	                if s.get("skip") is not None and i == int(s["skip"]):
   255	                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
   256	                idx = np.flatnonzero(~mv[:, i])
   257	                if s.get("fast") and mv[:, i].any():
   258	                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
   259	                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
   260	                if not len(idx):
   261	                    continue
   262	                last = idx[-1]
   263	                first = last
   264	                while first - 1 >= 0 and not mv[first - 1, i]:   # 最後の止まりが続いたところの頭
   265	                    first -= 1
   266	                rec["hold_err"][i] = err[last, i]
   267	                rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
   268	                rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
   269	            if s.get("skip") is not None:
   270	                rec["lag_max"][int(s["skip"])] = 0.0
   271	                rec["away_max"][int(s["skip"])] = 0.0
   272	            ce = np.abs(err)
   273	            if s.get("skip") is not None:
   274	                ce[:, int(s["skip"])] = 0.0
   275	            rec["coarse_max"] = ce.max(axis=0)
   276	            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
   277	        elif n:
   278	            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
   279	        out.append(rec)
   280	    return out
   281	
   282	
#!/usr/bin/env python3
"""diag_joint_hold.py — 実機推論の記録から, 関節ごとの「動いている途中の遅れ」と「止まったあとのずれ (垂れ)」を出す (2026-11-01 依頼 57).

読むだけ (runs/ には書かない). アームは使わない.

入力: runs/pi05_policy/{run_stem}/recording/states.csv と log.jsonl
  states.csv (依頼 46 のあとの記録) には send_angles 行 (送った時刻・目標・rpc_sec) と, 爪の rpc 行がある.
  依頼 46 より前の記録 (send_angles 行がない) は log.jsonl だけで見る (下の「log で見る」).

分け方 (states.csv で見る):
  - 指令 = 直前の send_angles の目標. 送ってから rpc が返るまで (daemon が着いたと判定するまで. 約 1.6 秒) は
    つながりがふさがり, 実測は残らない. rpc が返ったあとから次の送信までを「止まっているはずの区間」(hold) とする.
    爪の rpc (開け閉め. 指令は変わらない) でも区間を切る (爪の前・爪の後 を別に見る).
  - 区間の中のコマごと・関節ごとに, 前後 MOVE_WIN_SEC 以内の実測のふれが MOVE_EPS_DEG を超えていれば「動いている」.
    動いているコマの |実測 − 指令| を「動いている途中の遅れ」とする (着いたと判定されたあとも動いていた分も含む).
  - 動いていないコマの |実測 − 指令| を「止まったあとのずれ」とする. 関節ごとに, 区間の最後の動いていないコマの値を
    その区間のずれとし (区間の数で数える. 長く待った区間が重くならない), run ごとに中央値・90%・最大を出す.
    codex R1 必須: 動いているかは関節ごとに見る (j1 が動き続けていても, 止まった j4 のずれを見落とさない).
    その関節が区間の中で一度も止まっていなければ, その区間のずれは無し (NaN) とする.
  - 区間の最後の「止まりが続いたところ」(関節ごと. つながった動いていないコマ) の変化 (最後 − 最初) を
    「止まっている間の変化」(じわじわ垂れる) とし, 続いた秒も出す. MOVE_EPS_DEG に満たない速さで動き続けるものはここに出る.
  - 前後 0.3 秒の窓で動きが見えなかったコマを「動いていない」としている (区間の端・まばらな記録では 1 コマでも
    動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
  - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.

ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)

log で見る (依頼 46 より前の記録とも比べられる):
  - 補正の 1 刻み: measured_after − after (全 6 関節)
  - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)

Usage:
  python3 pi05/diag_joint_hold.py --run pi05_20261101_190347 --run pi05_20261101_194042
  python3 pi05/diag_joint_hold.py --run ... --json docs/diag_joint_hold_20261101.json
"""
import argparse
import csv
import json
import sys
from pathlib import Path

import numpy as np

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
RUNS_ROOT = DATA_ROOT / "runs" / "pi05_policy"
JOINTS = ["j0", "j1", "j2", "j3", "j4", "j5"]
MOVE_WIN_SEC = 0.3      # 前後これだけの間の実測のふれで「動いている」を見る
MOVE_EPS_DEG = 0.3      # ふれがこれを超えたら動いている (読みの刻みは約 0.09°)
LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いていないコマがこれだけ続いた区間で見る
# 指令が変わらないのに動いた・止まったあとも残ったずれがこれを超えた区間を「外れ」として数える (依頼 57).
# 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
# Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
EVENT_DEG = 4.0
# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
FAST_RPC_SEC = 0.5
PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限

exec
/bin/zsh -lc "python3 -B - <<'PY'
import sys
sys.path.insert(0,'pi05')
import diag_joint_hold as d
import numpy as np

def seg(cmd, vals, fast=True, skip=None):
 q=np.zeros((len(vals),6)); q[:,4]=vals
 c=np.zeros(6); c[4]=cmd
 return dict(kind='send',stage='model',stages=['model']*len(vals),cmd=c,t_cmd=0,t0=0,ts=np.arange(len(vals))*.1,q=q,fast=fast,skip=skip)
a=seg(0,[0,1,2,3]); z=seg(0,[3,4,5,6])
for name,b in [('slow_empty',seg(0,[],False)),('changed_empty',seg(10,[])),('hold_empty',seg(0,[],skip=4)),('unknown',dict(seg(0,[3]),cmd=np.full(6,np.nan)))]:
 out=d.analyze_segments([a,b,z]); print(name,'away=',out[-1]['away_max'][4])
import test_diag_joint_hold as t
import unittest
unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(t.TestToday))
PY" in /Users/shimazueiichi/r2r2r-scripts
 exited 1 in 97ms:
zsh:1: can't create temp file for here document: operation not permitted

codex
pad の最近傍対応と、実測がある fast 区間同士の引き継ぎは意図に合っています。一方、実測がない区間では引き継ぎの解除処理も通らないため、指令変更や長押しを挟んだ場合をメモリ上で確認します。
exec
/bin/zsh -lc "python3 -B -c 'import sys; sys.path.insert(0,\"pi05\")
import diag_joint_hold as d
import numpy as np
def seg(cmd, vals, fast=True, skip=None):
 q=np.zeros((len(vals),6)); q[:,4]=vals
 c=np.zeros(6); c[4]=cmd
 return dict(kind=\"send\",stage=\"model\",stages=[\"model\"]*len(vals),cmd=c,t_cmd=0,t0=0,ts=np.arange(len(vals))*.1,q=q,fast=fast,skip=skip)
a=seg(0,[0,1,2,3]); z=seg(0,[3,4,5,6])
for name,b in [(\"slow_empty\",seg(0,[],False)),(\"changed_empty\",seg(10,[])),(\"hold_empty\",seg(0,[],skip=4)),(\"unknown\",dict(seg(0,[3]),cmd=np.full(6,np.nan)))]:
 out=d.analyze_segments([a,b,z]); print(name,\"away=\",out[-1][\"away_max\"][4])
import test_diag_joint_hold as t
import unittest
unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(t.TestToday))'" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 769ms:
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-is4N1Beg' (errno=Operation not permitted)
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-erfcBTpl' (errno=Operation not permitted)
test_20261106_gamepad (test_diag_joint_hold.TestToday)
依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ. ... ok
test_coarse_and_events (test_diag_joint_hold.TestToday) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.377s

OK
slow_empty away= 6.0
changed_empty away= 6.0
hold_empty away= 6.0
unknown away= 6.0

exec
/bin/zsh -lc "cat AGENTS.md; rg -n 'resends|t_send|t_halt' tools/gamepad_teleop.py; cat docs/review_request_20261106_corr_v5_gamepad_r2.md" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 107ms:
# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

## Project Overview

R2R2R (Real-to-Real-to-Real) is a robotic pick-and-place system using a **mechArm 270-Pi** robot arm controlled from a **Mac + Raspberry Pi** setup. The system uses camera-based red block detection with homography coordinate transformation to perform automated pick-and-place operations, with VLM (Codex API) verification of results.

## Architecture

**Two-machine split**: Mac handles vision/coordination, Pi controls the robot arm.

- **Mac side** (`camera_pick_mac.py`): Captures camera frames, detects red blocks via HSV color segmentation, converts pixel coords to robot coords using homography, then SSH+SCP to Pi to execute arm movements.
- **Pi side** (`camera_pick_pi.py`): Receives robot XY coordinates as CLI args, connects to mechArm via serial (`/dev/ttyAMA0` at 1000000 baud), executes pick sequence (ABOVE → APPROACH_PICK → PICK → grip → retract → HOME).
- **Loop runner** (`r2r2r_loop.py`): Orchestrates `pick_and_place_fixed.py` via SSH with up to 3 retries, uses VLM (`vlm_judge.py`) to verify success by checking if the red block landed in the target container.

### Key Modules

- `detect_block_position.py` — Red block detection (HSV thresholding) + `pixel_to_world()` homography transform. Imported by `camera_pick_mac.py`.
- `vlm_judge.py` — Captures a frame and sends to Codex API (Codex-sonnet-4-20250514) to judge if the block is in the target container. Imported by `r2r2r_loop.py`.
- `calibrate_homography.py` — Interactive 4-point calibration tool. Click 4 corners on camera view, input real-world mm coords, saves `homography.npy`.
- `pick_and_place_fixed.py` — Fixed-angle pick-and-place sequence (runs on Pi). Uses predefined joint angles, not camera-guided.

### Coordinate Pipeline

```
iPhone camera (Iriun Webcam, index 1)
  → pixel (u,v)
  → homography.npy (pixel → Robot mm)
  → Robot XY (mm) sent to Pi via SSH
  → Pi applies gripper offset (-20mm X, -80mm Y)
  → send_coords() with nearest calibration point's RXYZ
```

## Hardware Configuration

- **Robot**: mechArm 270-Pi, serial `/dev/ttyAMA0` @ 1000000 baud, `pymycobot` library
- **Camera**: Iriun Webcam (iPhone → Mac virtual camera), OpenCV index 1
- **SSH**: `ubuntu@192.168.2.1` via `~/.ssh/id_rsa_pi` (wired Ethernet)
- **Home position**: `send_angles([0, 0, 0, 0, 0, -30], 20)`
- **Gripper**: open = `set_gripper_state(0, 100)`, close = `set_gripper_state(1, 70)`

## Running Scripts

All scripts run as standalone Python3. Mac-side scripts require `opencv-python`, `numpy`, `paramiko`. Pi-side scripts require `pymycobot`.

```bash
# Camera-guided pick (run on Mac)
python3 camera_pick_mac.py

# Fixed-angle pick-and-place loop with VLM verification (run on Mac)
python3 r2r2r_loop.py

# Recalibrate homography (run on Mac, interactive)
python3 calibrate_homography.py

# Detect block position only (run on Mac)
python3 detect_block_position.py
```

## Current Phase (DAY151完了)
- Phase 1 ✅ 完了: r2r2r_loop.py で SUCCESS達成
- Phase 2 🔄 進行中: 成功率計測・CSV logging
- Phase 3 未着手: カメラベース失敗検知

## Lookup Table (FK Scan)
- fk_scan_motion_based.csv: 134,456点 (8姿勢起点 ±15°/5°刻り)
- fk_scan_table.csv: 481点 (PICK起点 j0全域)
- カラム構造: ee_x, ee_y, ee_z, j0, j1, j2, j3, j4, j5

## Next Task (DAY152)
Pi側 nearest-neighbor lookup実装:
  pixel → homography → Robot XY → fk_scan_motion_based.csvから最近傍 → send_angles()
補間関数:
  interpolate_angles(start, end, steps=5, easing=True)
  get_angles()で現在角度取得 → 5ステップ補間 → sleep=0.05s

## 禁止事項
- git clean 絶対禁止
- git add . 禁止（個別ファイル指定）
- homography.npy の値を変更しない
- ROI (1571, 607, 249, 211) 変更禁止
- time.sleep() を削除・短縮しない
- Calibration values in `calibration_reference.md` are measured/confirmed values — do not change without physical re-measurement
- Unity physics values (Stiffness: 20000, Damping: 2000) are locked
- Scripts transferred to Pi via SCP before each execution (always the latest local version)

## 記事制作ルール（SNS出力）
記事MD・JSONを出力した後、必ず以下をplain textで全部出力すること：
- X投稿文
- Facebook投稿文
- LinkedIn本文（英語→日本語）
- LinkedIn補足コメント（英語→日本語）

コードブロック不使用。省略禁止。

## AIツール分担（更新）
- Codex: コード実装・ファイル操作・ログ確認（メイン）
- Cline: どうしても必要な場合のみ（基本不使用）
- Codex.ai: 設計・ドキュメント・記事制作
- エイイチ: Unityエディタ操作・最終判断

## 物理憲法（追加ルール）

### send_coords() 使用禁止（DAY152で確定）
- mc.send_coords() の IK は RZ値が位置によって +167°〜−140° で暴れる
- ワークスペース全域で IK が不安定なため使用不可
- すべての動作は mc.send_angles() + lookup table 経由で実装する
- 例外なし。最適化や一時的な実験目的でも使用禁止
- 違反した場合、エイイチが即座に指摘・修正を要求

## FKスキャナー命名規則（DAY158で確定）

FKスキャナーのクラス名・ファイル名・GameObject名・ログプレフィックスは全て以下で統一する：

- `FKScanner_XPos` : Unity X正方向（赤ブロック側）、旧 LowerLeftFKScanner
- `FKScanner_XNeg` : Unity X負方向(白箱側)、旧 PlaceRightFKScanner
- `FKScanner_Central` : j0中央帯(補助)、旧 CentralFKScanner
- `FKScanner_MotionBased` : 運用ベース統合、旧 ParallelFKScanner

命名パターン:
- 単一軸指定:  `FKScanner_{軸}{Pos|Neg}`  例: `FKScanner_XPos`
- 複数軸指定:  `FKScanner_{軸1}{符号1}_{軸2}{符号2}`  例: `FKScanner_XPos_ZNeg`
- 意味ベース:  `FKScanner_{意味}`  例: `FKScanner_Central`, `FKScanner_MotionBased`

旧名（PlaceRight / LowerLeft 等の方向系命名）は**永久廃止**。
CSV出力名は互換性保持のため現行維持（`fk_scan_motion_based.csv` 等）。
Pi側 `r2r2r_loop.py` は `fk_scan_motion_based.csv` を参照する（リネーム対象外）。
# AGENTS.md 追記用：メソドロジー運用ルール

以下のセクションを既存の `AGENTS.md` の末尾または適切な位置に追記してください。

---

## メソドロジー運用ルール（DAY167以降）

Pixels to Motorsの実践から得られる判断軸・設計思想を、
他者が応用可能なメソドロジーとして蓄積する仕組み。

### 基本方針

- 目的：個人プロジェクトを「メソドロジー」として言語化し、将来的な発信・輸出の素材とする
- ペース：**週2回**（月・木を目安）
- 書けない日はスキップする（無理に絞り出さない）
- 実作業を圧迫しない範囲で運用する

### ファイル構成

```
~/r2r2r-content/methodology/
├── principles.md          # 原則の蓄積ファイル（Pxxx番号で管理）
└── day_log_template.md    # DAYログ用テンプレート（メソドロジー版）
```

### DAYログへの組み込み方

既存のDAYログフォーマットに、以下のセクションを追加する：

1. **判断ログ**（任意）: 複数の選択肢から1つを選んだ場合のみ記録
2. **今日の原則**（週2回程度）: 新規原則または既存原則の適用事例

### 新規原則の登録フロー

1. DAYログに「今日の原則」を1〜2文で記述
2. 分類タグを付与（`#design` / `#workflow` / `#tool` / `#safety` / `#ai-collab` / `#hardware` / `#documentation`）
3. `principles.md` にP{NNN}として転記
4. 既存原則と重複しないか確認（重複なら既存Pxxxの「適用例」への追記に切り替え）

### 既存原則の適用事例を記録するフロー

1. DAYログに `P{NNN}「原則名」が今日も効いた` と記述し、具体例を添える
2. `principles.md` の該当原則の「適用例」セクションにDAY番号と一行事例を追記

### 原則の抽象度ルール

- ❌ 低すぎる例：「FixedJointは安定する」（個別技術で終わる）
- ❌ 高すぎる例：「シンプルな設計が良い」（当たり前で新規性なし）
- ✅ 適切な例：「状況＋行動＋優先順位」の3要素を含む

### Codex / Codexへの依頼時のガイドライン

- DAYログを書く際、「メソドロジー版テンプレートに従って」と指示する
- 新規原則を登録する際は「principles.mdにP{次の番号}として追記」と明示
- 原則の重複チェックは、principles.md全体を読んでから判断する

### 禁止事項

- 原則を毎日書こうとしない（週2を死守しない）
- 原則のインフレ（量産）を避ける
- 実作業の時間を削ってまで原則を言語化しない
- 過去のDAYログを遡って原則を抽出する作業は**しない**（これから書くものだけ蓄積）

### 3ヶ月後の目標

- 原則数：20〜25個
- 次のマイルストーン：LinkedIn向けまとめコンテンツ
  `"The Pixels-to-Motors Method: N Principles for Designers Entering Physical AI"` を執筆可能な状態にする
484:            t_send = time.time()
488:                return {"sent": "unknown", "error": f"send_failed: {type(e).__name__}: {e}", "t_send": t_send}
491:            return {"sent": True, "reached": None, "no_check": True, "t_send": t_send}
497:        t_send = time.time()
503:                    "measured_before_send": _r(pre), "t_send": t_send}
507:        t_last_send = t0
511:        max_resends = STEP_MAX_RESENDS if step else IGNORED_MAX_RESENDS
514:        resends = []                      # 送り直した時刻 (送ってからの秒)
515:        base = {"sent": True, "timeout_sec": round(timeout, 2), "t_send": t_send, "measured_before_send": _r(pre),
516:                "resends": resends}
547:            if (len(resends) >= max_resends and moved < IGNORED_MOVE_DEG
548:                    and time.monotonic() - t_last_send >= check_sec):
552:            if (len(resends) < max_resends and moved < IGNORED_MOVE_DEG
553:                    and time.monotonic() - t_last_send >= check_sec):
564:                t_last_send = time.monotonic()
565:                resends.append(round(t_last_send - t0, 2))
576:        t_send = time.time()
577:        out = {"t_halt": t_send}
644:        t_send = time.time()
649:            return {"sent": True, "reached": None, "no_check": True, "t_send": t_send, "dry": True}
654:                "t_send": t_send, "dry": True}
816:                self.request_halt(reason, hold_id=h["id"], joint=h["joint"])
819:    def request_halt(self, reason, hold_id=None, joint=None):
877:            self.request_halt(reason)
1089:                        if (hs is not None and hs["resends"] < HOLD_MAX_RESENDS
1221:                        self.hold_sent["resends"] += 1
1226:                    self.hold_sent = {"q": list(mb), "last_q": list(mb), "t": time.monotonic(), "resends": 0}
1229:                self.request_halt("released_while_sending", hold_id=e["hold_id"], joint=job[1])
1273:            if res.get("resends"):
1274:                how += f"・無視されたので {len(res['resends'])} 回送り直した"
1605:                       "ignored_check_sec": IGNORED_CHECK_SEC, "ignored_max_resends": IGNORED_MAX_RESENDS,
# コードレビュー依頼 R2：diag_joint_hold の pad の組み方と away の引き継ぎ（2026-11-06 依頼 97）

R1（docs/review_result_20261106_corr_v5_gamepad_codex.md）の必須 2 件と推奨 1 件を pi05/diag_joint_hold.py に反映した。

1. 必須（pad 送信の対応付け）: `_pad_send_for(sends, used, ts)` を、まだ組にしていない送信のうち |t_send − ts| ≤ 0.2 秒でいちばん近いものを初めての送信とし、なければ ts より前に組にした送信のうちいちばん新しいものの送り直し（resends の数まで）とする形にした。どちらもなければ pad_unknown
2. 必須（区間をまたぐ垂れ）: すぐ返った区間がつづき、その関節の指令が変わらない間は、関節ごとに最小の |実測−指令| を区間をまたいで引き継ぐ（指令が変わる・すぐ返らない区間・長押しの関節では引き継がない）
3. 推奨（stop の対応）: halt もいちばん近い・まだ組にしていないものと一対一で組む

テスト: pi05/test_diag_joint_hold.py に 2 件（次の送信を拾わない・0→3° と 3→6° の垂れを 6° と見る／指令が変われば引き継がない）。21 件 OK。
11-01 の 9 本は前の結果と全部同じ。11-06 の 5 本: 入れる 4 本は外れ 0（j4 の離れた分の最大 3.8°（184452））、185554 は外れ 5 区間（j4）。docs/diag_joint_hold_20261106.txt・.json を作り直した。

差分は `git diff -- pi05/diag_joint_hold.py pi05/test_diag_joint_hold.py` で見られます。
ほかの推奨（184650 の範囲・1 コマの動き・偽物の assets）は報告に書いて据え置きます。

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

exec
/bin/zsh -lc "python3 -B -c 'import sys,json; sys.path.insert(0,\"pi05\"); import diag_joint_hold as d
for day in (\"20261101\",\"20261106\"):
 old=json.loads(open(\"docs/diag_joint_hold_\"+day+\".json\").read()); runs=old[\"runs\"]
 for stem,expected in runs.items():
  got,segs=d.summarize_run(d.RUNS_ROOT/stem)
  diff=[k for k,v in expected.items() if got.get(k)"'!=v]
  print(stem,"different_keys",diff,"events",got.get("event_segments"),"j4away",got.get("away_max",{}).get("j4"))
  if day=="20261106":
   barriers=[(s["kind"],s["fast"],s.get("skip")) for s in d.segments(d.load_rows(d.RUNS_ROOT/stem/"recording/states.csv"),[json.loads(l) for l in open(d.RUNS_ROOT/stem/"log.jsonl")]) if not len(s["ts"])]
   print("empty",barriers)
'"'" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 785ms:
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-le1nR0Y4' (errno=Operation not permitted)
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-EEwYBlWe' (errno=Operation not permitted)
pi05_20261101_190347 different_keys [] events 0 j4away 0.0
pi05_20261101_190854 different_keys [] events 0 j4away 0.0
pi05_20261101_191547 different_keys [] events 0 j4away 0.0
pi05_20261101_192101 different_keys [] events 0 j4away 0.0
pi05_20261101_192418 different_keys [] events 2 j4away 0.0
pi05_20261101_192758 different_keys [] events 2 j4away 0.0
pi05_20261101_193041 different_keys [] events 2 j4away 0.0
pi05_20261101_193411 different_keys [] events 3 j4away 0.0
pi05_20261101_194042 different_keys [] events 4 j4away 0.0
pi05_20261031_175527 different_keys [] events None j4away None
pi05_20261031_180435 different_keys [] events None j4away None
pi05_20261031_181946 different_keys [] events None j4away None
pi05_20261031_182545 different_keys [] events None j4away None
pi05_20261031_184016 different_keys [] events None j4away None
pi05_20261106_183926 different_keys [] events 0 j4away 1.23
empty []
pi05_20261106_184452 different_keys [] events 0 j4away 3.78
empty []
pi05_20261106_184650 different_keys [] events 0 j4away 2.73
empty []
pi05_20261106_184906 different_keys [] events 0 j4away 2.37
empty [('pad_hold', True, 1)]
pi05_20261106_185554 different_keys [] events 5 j4away 5.8
empty []

codex
**必須修正が1件残っています。** pad と stop の対応付けは修正意図に合っていますが、away の引き継ぎ解除が、実測のない区間で働きません。今回の5本の集計結果は再現できました。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| 実測のない区間での引き継ぎ解除 | **必須** | [analyze_segments](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/diag_joint_hold.py:233) の carry 更新・解除が `if n ...` の内側にあります。このため、実測0件の「指令変更」「遅いRPC」「長押し」を挟んでも古い最小誤差が残ります。指令0°・実測0→3° → 実測なしの指令10° → 指令0°・実測3→6°で、最後の away が **3°ではなく6°**になることを再現しました。解除判定は実測件数に関係なく各区間で行い、指令変更・skip は該当関節、非fast・指令不明は全関節の carry を解除してください。 |
| 境界条件のテスト | **推奨** | 上記の実測0件の3条件と、`pad_unknown` を挟む場合を追加してください。現在の追加テストでは、実測のある区間での指令変更だけを確認しています。stop の最近傍・消費済みhaltの再利用禁止も回帰テストにすると有用です。 |
| pad送信の対応付け | 据え置き：修正確認 | 未使用・±0.2秒以内の最近傍を初回にし、なければ最新の対応済み送信へ再送数まで対応する実装になっています。初回＋再送数の回数管理も整合しています。 |
| 実測のあるfast区間のaway | 据え置き：修正確認 | 指令不変の関節で最小誤差を保持し、別関節の指令変更をまたいだ垂れを検出できます。長押し対象関節の除外も、実測がある区間では機能しています。 |
| stopの対応付け | 据え置き：修正確認 | 範囲内の最近傍・未使用haltを一対一で対応させています。既知のhalt_jointだけ指令を置き換える処理も維持されています。 |
| 11-01との互換性 | 据え置き：再確認済み | 9本について、既存JSONに保存された全結果項目が再計算と一致しました。 |
| 11-06の選別結果 | 据え置き：再現済み | 5本とも保存JSONと一致。採用4本は外れ0、184452のj4 away最大は3.78°。185554はj4の外れ5区間です。 |
| 184650の範囲・1コマの動き・偽物のassets | 据え置き | R1の推奨事項として残す扱いでよいです。今回の修正で解消した項目には含めません。 |

読み取り専用の実ログテスト2件は成功しました。全21件のうち一時ファイルを作るテストは実行せず、追加の境界条件はメモリ上で検証しました。ファイル変更、実機・Pod・Hugging Faceの操作は行っていません。

exec
/bin/zsh -lc "sed -n '1,300p' pi05/test_diag_joint_hold.py" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 132ms:
#!/usr/bin/env python3
"""test_diag_joint_hold.py — diag_joint_hold.py (依頼 57) の確かめ.

  - 動いている途中の遅れと, 止まったあとのずれを分ける (rpc が返ったあとの実測だけを使う)
  - 指令が変わらないのに動いた区間を「外れ」として数える. HOME は数えない
  - 爪の rpc で区間を切る
  - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
  - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
"""
import csv
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

import numpy as np

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))

import diag_joint_hold as d  # noqa: E402

COLS = ["ts", "event_type", "stage", "j0", "j1", "j2", "j3", "j4", "j5", "speed", "gripper_state",
        "ts_send", "ts_arrival", "attempt", "arrived", "note"]


class Rec:
    def __init__(self):
        self.rows = []

    def send(self, t, q, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})

    def rpc(self, t, cmd, stage="correct", rpc=0.03):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})

    def grip(self, t, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})

    def actual(self, t0, t1, q, stage="model", dt=0.1):
        t = t0
        while t < t1 - 1e-9:
            qq = q(t) if callable(q) else q
            self.rows.append({"ts": round(t, 3), "event_type": "actual", "stage": stage, **_j(qq)})
            t += dt

    def write(self, run_dir, log=()):
        rec = run_dir / "recording"
        rec.mkdir(parents=True)
        with (rec / "states.csv").open("w", newline="") as f:
            w = csv.DictWriter(f, fieldnames=COLS)
            w.writeheader()
            for r in self.rows:
                w.writerow({k: r.get(k, "") for k in COLS})
        (run_dir / "log.jsonl").write_text("".join(json.dumps(x) + "\n" for x in log))
        (run_dir / f"{run_dir.name}.json").write_text(json.dumps({"outcome": "success", "corrections": 1}))


def _j(q):
    return {f"j{i}": f"{v:.3f}" for i, v in enumerate(q)}


def pose(j4, j1=10.0):
    return [0.0, j1, 0.0, 0.0, j4, -30.0]


class TestSegments(unittest.TestCase):
    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_x"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_hold_vs_lag_and_events(self):
        r = Rec()
        # 1. 送って rpc が返ったあと, 指令から 1° ずれて止まる (ふつうの止まり)
        r.send(0.0, pose(-50))
        r.actual(1.5, 4.0, pose(-49))
        # 2. 送ったあと 2 秒かけて -60 → -55 へ動き (指令は -60), そのまま止まる (外れ: 指令なしに +5°)
        r.send(4.0, pose(-60))
        r.actual(5.5, 7.5, lambda t: pose(-60 + 5 * min(1.0, (t - 5.5) / 1.0)))
        r.actual(7.5, 9.0, pose(-55))
        # 3. HOME で戻らない (数えない)
        r.send(9.0, pose(0), stage="home")
        r.actual(10.5, 12.0, pose(40), stage="home")
        res, segs = self.build(r)
        self.assertEqual(len(segs), 3)
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 5.0, places=1)       # 止まったあとのずれ (2.)
        self.assertAlmostEqual(res["lag_max"]["j4"], 5.0, places=1)           # 動いている途中 (2. の頭は 0° ずれ… 最後の動きで 5°)
        self.assertEqual(res["hold_by_context"]["model"]["n"], 2)
        ev = res["events"]
        self.assertEqual([(e["sec"], e["joint"]) for e in ev], [(4.0, "j4")])  # HOME は数えない
        self.assertAlmostEqual(ev[0]["hold_err"], 5.0, places=1)
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 40.0, places=1)
        self.assertAlmostEqual(res["coarse_max"]["j4"], 40.0, places=1)       # 粗い値は HOME も含む
        self.assertAlmostEqual(res["hold_signed_median"]["j1"], 0.0, places=2)

    def test_no_samples_during_rpc_and_still_threshold(self):
        r = Rec()
        r.send(0.0, pose(-50), rpc=1.6)
        r.actual(0.0, 1.5, pose(-30))            # rpc の途中の値 (使わない)
        r.actual(1.6, 3.0, pose(-50.2))
        res, segs = self.build(r)
        self.assertEqual(segs[0]["n"], 14)
        self.assertEqual(res["events"], [])
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 0.2, places=2)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_grip_splits_segment(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 2.5, pose(-50))
        r.grip(2.5)
        r.actual(4.0, 5.0, pose(-46))             # 爪のあとで 4° 動いていた (外れの手前)
        res, segs = self.build(r)
        self.assertEqual([s["kind"] for s in segs], ["send", "grip_after"])
        self.assertEqual(res["grip"][0]["delta"][4], 4.0)
        self.assertAlmostEqual(res["hold_by_context"]["grip_after"]["j4_max"], 4.0, places=2)
        self.assertEqual(res["events"], [])        # 4.0 は外れの目安 (4° を超える) に入らない

    def test_drift_while_still(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 6.5, lambda t: pose(-50 + 0.2 * (t - 1.5)))   # 0.2°/秒 でじわじわ (動いているとはしない)
        res, _ = self.build(r)
        self.assertAlmostEqual(res["drift_max"]["j4"], 1.0, delta=0.05)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_joint_wise_still(self):
        """codex R1 必須: j1 が動き続けていても, 止まった j4 の 5° のずれを見落とさない."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))   # j1 は 2°/秒 で動き続ける. j4 は -55 で止まり
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][1]))                    # j1 は一度も止まっていない
        self.assertAlmostEqual(segs[0]["hold_err"][4], 5.0, places=2)
        self.assertEqual([e["joint"] for e in res["events"]], ["j1", "j4"])
        self.assertEqual(res["event_segments"], 1)                           # 区間の数は 1
        self.assertIsNone(res["hold"]["j1"])

    def test_grip_after_not_still_is_none(self):
        """codex R2 必須: 爪のあとに止まらなかった関節のずれは None (最初の実測のずれで埋めない)."""
        r = Rec()
        r.send(0.0, pose(-50, j1=10))
        r.actual(1.5, 2.5, pose(-50, j1=10))
        r.grip(2.5)
        r.actual(4.0, 6.0, lambda t: pose(-50, j1=10 + 2.0 * (t - 4.0)))
        res, _ = self.build(r)
        g = res["grip"][0]
        self.assertIsNone(g["err_after"][1])
        self.assertEqual(g["err_after"][4], 0.0)
        self.assertEqual(g["err_first"][1], 0.0)

    def test_event_segments_by_index(self):
        """codex R2 推奨: 秒が近い別の区間も別に数える."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.01)
        r.actual(0.01, 0.03, pose(-50))
        r.send(0.04, pose(-60), rpc=0.01)
        r.actual(0.05, 1.0, pose(-50))
        res, _ = self.build(r)
        self.assertEqual(res["event_segments"], 2)

    def test_home_only(self):
        """codex R1 必須: HOME の区間しかない記録でも止まらない."""
        r = Rec()
        r.send(0.0, pose(0), stage="home")
        r.actual(1.5, 3.0, pose(1), stage="home")
        res, _ = self.build(r)
        self.assertEqual(res["work_segments"], 0)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertIsNone(res["hold"]["j4"])
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 1.0, places=2)

    def test_drift_uses_last_still_run(self):
        """codex R1 推奨: 止まり中の変化は最後の止まりが続いたところだけ (途中で動いた分を入れない)."""
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 3.0, pose(-50))
        r.actual(3.0, 3.5, lambda t: pose(-50 + 10 * (t - 3.0)))           # 途中で 5° 動く
        r.actual(3.5, 6.0, pose(-45))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["drift"][4], 0.0, places=2)
        self.assertGreater(segs[0]["still_sec"][4], 1.5)
        self.assertLess(segs[0]["still_sec"][4], 2.5)

    def test_json_has_no_nan(self):
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))
        td = Path(tempfile.mkdtemp())
        r.write(td / "pi05_y")
        out = td / "o.json"
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--timeline", "pi05_y",
                                     "--json", str(out)]), mock.patch("sys.stdout"):
            d.main()
        self.assertNotIn("NaN", out.read_text())
        json.loads(out.read_text())
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--json", str(out)]), \
                mock.patch("sys.stdout"), self.assertRaises(SystemExit):
            d.main()                                                          # 既にあるファイルには書かない

    def test_log_hold_errors(self):
        log = [
            {"chunk": 0, "t": 1.0, "executed": True, "state_source": "measured",
             "measured_state": pose(-1) + [0.0], "sent_targets": [pose(-40)]},
            {"chunk": 1, "t": 2.0, "executed": False, "state_source": "target",
             "measured_state": pose(-37) + [0.0], "sent_targets": [pose(-45)]},
            {"type": "correction", "action": "move", "sent": True, "t": 3.0,
             "after": pose(-45), "measured_after": pose(-30)},
            {"type": "rewind", "t": 4.0},
            {"chunk": 2, "t": 5.0, "executed": True, "state_source": "measured", "measured_state": pose(9) + [0.0]},
        ]
        out = d.log_hold_errors(log)
        self.assertEqual([round(float(e[4]), 2) for _, e in out["chunk"]], [3.0])     # 実行していないチャンクの目標は使わない. 巻き戻しのあとは比べない
        self.assertEqual([round(float(e[4]), 2) for _, e in out["correction"]], [15.0])

    def test_old_record_without_send_rows(self):
        r = Rec()
        r.actual(0.0, 2.0, pose(-50))
        res, segs = self.build(r)
        self.assertFalse(res["has_send_rows"])
        self.assertEqual(segs, [])


def pad_move(t, command, joint, hold=False, resends=(), cid=1):
    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
         "joint": joint, "resends": list(resends)}
    if hold:
        r["no_check"] = True
    return r


def pad_halt(t, q, joint, cid=1):
    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
            "halt_joint": joint, "halt": "stop"}


class TestPadAndFast(unittest.TestCase):
    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""

    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_p"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_fast_approach_is_not_lag(self):
        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
        r.actual(2.3, 3.5, pose(-59.5))
        res, segs = self.build(r)
        self.assertEqual(res["fast_segments"], 1)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
        self.assertLess(res["away_max"]["j4"], 0.2)

    def test_fast_hold_ignores_still_before_moving(self):
        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.8, pose(-50))
        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
        self.assertEqual(res["events"], [])

    def test_fast_sag_after_arrival_is_event(self):
        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 1.0, pose(-60))
        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
        r.actual(1.6, 3.0, pose(-54))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)

    def test_pad_step_hold_stop(self):
        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
        start = pose(-60, j1=40)
        step = pose(-60, j1=40); step[0] = 1.0
        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
        r = Rec()
        r.send(0.0, start, stage="model", rpc=0.03)
        r.actual(0.03, 1.0, start, stage="model")
        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
        r.actual(2.03, 3.0, step, stage="correct")
        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)

exec
/bin/zsh -lc "sed -n '210,290p' pi05/diag_joint_hold.py; rg -n '"'^def |''^class |temp|write|open'"\\(' pi05/test_diag_joint_hold.py" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 109ms:
    mv = np.zeros((n, 6), dtype=bool)
    for i in range(n):
        m = np.abs(ts - ts[i]) <= win
        mv[i] = (q[m].max(axis=0) - q[m].min(axis=0)) > eps
    return mv


def analyze_segments(segs):
    """区間ごとの値 (関節ごと. 止まっていない関節は NaN).
    戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
    out = []
    nan6 = np.full(6, np.nan)
    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
    carry_cmd, carry_min = nan6.copy(), nan6.copy()
    for k, s in enumerate(segs):
        n = len(s["ts"])
        rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
               "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
               "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
        rec["away_max"] = np.zeros(6)
        rec["skip"] = s.get("skip")
        rec["fast"] = bool(s.get("fast"))
        # codex R2 必須: 引き継ぎを切るかは, 実測があるかに関係なく区間ごとに決める.
        # すぐ返らない区間・指令の分からない区間は全関節, 指令が変わった関節・長押しの関節はその関節を切る
        if not s.get("fast") or np.isnan(s["cmd"]).any():
            carry_cmd, carry_min = nan6.copy(), nan6.copy()
        else:
            changed = ~np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0)
            carry_min = np.where(changed, np.nan, carry_min)
            carry_cmd = s["cmd"].copy()
            if s.get("skip") is not None:
                carry_min[int(s["skip"])] = np.nan
        if n and not np.isnan(s["cmd"]).any():
            err = s["q"] - s["cmd"]
            mv = moving_mask(s["ts"], s["q"])
            if s.get("fast"):
                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
                a = np.abs(err)
                start = np.fmin(carry_min, a[0])                 # 切った関節 (NaN) は区間の頭から
                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
                rec["away_max"] = np.max(a - run_min, axis=0)
                carry_min = run_min[-1].copy()
                if s.get("skip") is not None:
                    carry_min[int(s["skip"])] = np.nan
                lag_mv = np.zeros_like(mv)
            else:
                lag_mv = mv
            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
            for i in range(6):
                if s.get("skip") is not None and i == int(s["skip"]):
                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                idx = np.flatnonzero(~mv[:, i])
                if s.get("fast") and mv[:, i].any():
                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                if not len(idx):
                    continue
                last = idx[-1]
                first = last
                while first - 1 >= 0 and not mv[first - 1, i]:   # 最後の止まりが続いたところの頭
                    first -= 1
                rec["hold_err"][i] = err[last, i]
                rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
            if s.get("skip") is not None:
                rec["lag_max"][int(s["skip"])] = 0.0
                rec["away_max"][int(s["skip"])] = 0.0
            ce = np.abs(err)
            if s.get("skip") is not None:
                ce[:, int(s["skip"])] = 0.0
            rec["coarse_max"] = ce.max(axis=0)
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        elif n:
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        out.append(rec)
    return out


14:import tempfile
27:        "ts_send", "ts_arrival", "attempt", "arrived", "note"]
30:class Rec:
50:    def write(self, run_dir, log=()):
53:        with (rec / "states.csv").open("w", newline="") as f:
55:            w.writeheader()
57:                w.writerow({k: r.get(k, "") for k in COLS})
58:        (run_dir / "log.jsonl").write_text("".join(json.dumps(x) + "\n" for x in log))
59:        (run_dir / f"{run_dir.name}.json").write_text(json.dumps({"outcome": "success", "corrections": 1}))
62:def _j(q):
66:def pose(j4, j1=10.0):
70:class TestSegments(unittest.TestCase):
72:        td = tempfile.mkdtemp()
74:        rec.write(run, log)
195:        td = Path(tempfile.mkdtemp())
196:        r.write(td / "pi05_y")
230:def pad_move(t, command, joint, hold=False, resends=(), cid=1):
238:def pad_halt(t, q, joint, cid=1):
243:class TestPadAndFast(unittest.TestCase):
247:        td = tempfile.mkdtemp()
249:        rec.write(run, log)
412:class TestToday(unittest.TestCase):

exec
/bin/zsh -lc "sed -n '310,490p' pi05/test_diag_joint_hold.py; sed -n '70,100p' pi05/diag_joint_hold.py" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 127ms:
        np.testing.assert_allclose(segs[1]["cmd"], step)
        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
        self.assertEqual(segs[2]["lag_max"][1], 0.0)
        self.assertEqual(segs[2]["skip"], 1)
        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
        self.assertEqual(res["events"], [])

    def test_pad_resend_and_uncommanded_wrist(self):
        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
        start = pose(-60, j1=50)
        far = pose(-60, j1=35)
        r = Rec()
        r.send(0.0, start, rpc=0.03)
        r.actual(0.03, 1.0, start)
        r.rpc(2.0, "send_angle")
        r.rpc(2.3, "send_angle")                                               # 送り直し
        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
        r.rpc(3.0, "stop")
        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)

    def test_pad_send_not_matched_to_next(self):
        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "send_angle")
        r.actual(1.03, 1.14, a_cmd, stage="correct")
        r.rpc(1.16, "send_angle")
        r.actual(1.19, 2.0, b_cmd, stage="correct")
        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
        _, segs = self.build(r, log)
        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)

    def test_away_carries_across_fast_segments(self):
        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10), rpc=0.03)
        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
        res, segs = self.build(r)
        self.assertLess(segs[0]["away_max"][4], 3.1)
        self.assertGreater(segs[1]["away_max"][4], 5.5)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        r2 = Rec()                                          # j4 の指令が変われば引き継がない
        r2.send(0.0, pose(-60), rpc=0.03)
        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
        r2.send(1.0, pose(-57), rpc=0.03)
        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
        res2, _ = self.build(r2)
        self.assertEqual(res2["events"], [])

    def test_carry_cut_by_segments_without_samples(self):
        """codex R2 必須: 実測のない区間 (指令が変わる・すぐ返らない・長押し・組にできない) を挟めば引き継がない.
        指令 0°・実測 0→3° → (実測なしの区間) → 指令 0°・実測 3→6° で, 最後の away は 3° (6° にしない)."""
        def run(between, log=()):
            r = Rec()
            r.send(0.0, pose(-60), rpc=0.03)
            r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
            between(r)
            r.send(1.2, pose(-60), rpc=0.03)
            r.actual(1.23, 2.2, lambda t: pose(-60 + 3 * (t - 0.2)))
            _, segs = self.build(r, log)
            return segs[-1]["away_max"][4]
        self.assertLess(run(lambda r: r.send(1.0, pose(-50), rpc=0.03)), 3.1)              # 指令が変わる
        self.assertLess(run(lambda r: r.send(1.0, pose(-60), rpc=1.5)), 3.1)               # すぐ返らない
        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model")), 3.1)       # 組にできない
        far = pose(-45)
        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model"),
                            [pad_move(1.0, far, 4, hold=True)]), 3.1)                        # j4 の長押し
        self.assertGreater(run(lambda r: r.grip(1.0, rpc=0.01)), 5.5)                      # 爪だけ: 引き継ぐ

    def test_halt_one_to_one(self):
        """codex R1 推奨: stop はいちばん近い・まだ組にしていない halt と組む (同じ halt を 2 回使わない)."""
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "stop", stage="correct")
        r.actual(1.03, 1.1, pose(-60, j1=35), stage="correct")
        r.rpc(1.15, "stop", stage="correct")
        r.actual(1.18, 2.0, pose(-60, j1=30), stage="correct")
        log = [pad_halt(1.0, pose(-60, j1=35), 1), pad_halt(1.15, pose(-60, j1=30), 1)]
        _, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_stop", "pad_stop"])
        self.assertAlmostEqual(segs[1]["cmd"][1], 35.0)
        self.assertAlmostEqual(segs[2]["cmd"][1], 30.0)


RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")


@unittest.skipUnless(RUNS.is_dir(), "KIOXIA がない")
class TestToday(unittest.TestCase):
    """依頼文の粗い値 (j4 の「直前に送った指令と実測の差」の最大) と同じになり, 入れる 3 本には外れがない."""
    COARSE = {"190347": 3.5, "190854": 2.5, "192101": 1.8, "192418": 10.3, "192758": 19.5,
              "193041": 18.4, "193411": 8.0, "194042": 59.5}

    def test_coarse_and_events(self):
        for stem, v in self.COARSE.items():
            res, _ = d.summarize_run(RUNS / f"pi05_20261101_{stem}")
            self.assertAlmostEqual(res["coarse_max"]["j4"], v, delta=0.051, msg=stem)
            n_ev = len([e for e in res["events"] if e["joint"] == "j4"])
            if stem in ("190347", "190854", "192101"):
                self.assertEqual(n_ev, 0, stem)
            else:
                self.assertGreater(n_ev, 0, stem)

    def test_20261106_gamepad(self):
        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
        for stem in ("183926", "184452", "184650", "184906", "185554"):
            p = RUNS / f"pi05_20261106_{stem}"
            if not p.is_dir():
                self.skipTest("11-06 の記録がない")
            res, _ = d.summarize_run(p)
            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
            self.assertGreater(res["fast_segments"], 0, stem)
            if stem == "185554":
                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
            else:
                self.assertEqual(res["events"], [], stem)


if __name__ == "__main__":
    unittest.main()
# Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
EVENT_DEG = 4.0
# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
FAST_RPC_SEC = 0.5
PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限


def load_rows(path):
    rows = []
    with Path(path).open(newline="") as f:
        for r in csv.DictReader(f):
            try:
                r["_ts"] = float(r["ts"])
            except Exception:
                continue
            rows.append(r)
    rows.sort(key=lambda r: r["_ts"])
    return rows


def _q(r):
    try:
        return np.array([float(r[j]) for j in JOINTS])
    except Exception:
        return None


def _rpc_sec(r):
    for tok in (r.get("note") or "").split():
        if tok.startswith("rpc_sec="):

codex
引き継ぎの解除判定は実測件数の判定より前に移され、R2の必須指摘に対応しています。追加テストの長押しケースは指令変更も同時に起きるため、長押しだけで解除できるかをメモリ上で補足確認します。
exec
/bin/zsh -lc "python3 -B - <<'PY'
import sys,json,unittest
from unittest.mock import patch
import numpy as np
sys.path.insert(0,'pi05')
import diag_joint_hold as d
import test_diag_joint_hold as t
def build(self,rec,log=()):
 rows=[dict(r,_ts=float(r['ts'])) for r in rec.rows]
 return None,d.analyze_segments(d.segments(rows,log))
suite=unittest.TestSuite(t.TestPadAndFast(n) for n in ('test_carry_cut_by_segments_without_samples','test_halt_one_to_one'))
with patch.object(t.TestPadAndFast,'build',build):
 unittest.TextTestRunner(verbosity=2).run(suite)
def seg(cmd,vals,skip=None):
 q=np.array([t.pose(v) for v in vals],float).reshape((-1,6))
 return dict(kind='send',stage='model',stages=['model']*len(vals),cmd=np.array(cmd,float),t_cmd=0,t0=0,fast=True,skip=skip,ts=np.arange(len(vals),dtype=float),q=q)
a=seg(t.pose(-60),[-60,-57]); empty=seg(t.pose(-60),[],4); b=seg(t.pose(-60),[-57,-54])
assert d.analyze_segments([a,empty,b])[-1]['away_max'][4]==3
print('same-command empty skip: PASS')
for day in ('20261101','20261106'):
 old=json.loads(open('docs/diag_joint_hold_'+day+'.json').read())
 for stem,expected in old['runs'].items():
  if day=='20261101' and not stem.startswith('pi05_20261101'): continue
  got,_=d.summarize_run(d.RUNS_ROOT/stem)
  diff=[k for k,v in expected.items() if got.get(k)"'!=v]
  print(stem,'"'different_keys',diff,'events',got.get('event_segments'),'j4away',got.get('away_max',{}).get('j4'))
PY" in /Users/shimazueiichi/r2r2r-scripts
 exited 1 in 128ms:
zsh:1: can't create temp file for here document: operation not permitted

exec
/bin/zsh -lc "python3 -B -c 'import sys,json,unittest; from unittest.mock import patch; import numpy as np; sys.path.insert(0,\"pi05\"); import diag_joint_hold as d; import test_diag_joint_hold as t
def build(self,rec,log=()):
 rows=[dict(r,_ts=float(r[\"ts\"])) for r in rec.rows]
 return None,d.analyze_segments(d.segments(rows,log))
suite=unittest.TestSuite(t.TestPadAndFast(n) for n in (\"test_carry_cut_by_segments_without_samples\",\"test_halt_one_to_one\"))
with patch.object(t.TestPadAndFast,\"build\",build): unittest.TextTestRunner(verbosity=2).run(suite)
def seg(vals,skip=None):
 return dict(kind=\"send\",stage=\"model\",stages=[\"model\"]*len(vals),cmd=np.array(t.pose(-60),float),t_cmd=0,t0=0,fast=True,skip=skip,ts=np.arange(len(vals),dtype=float),q=np.array([t.pose(v) for v in vals],float).reshape((-1,6)))
assert d.analyze_segments([seg([-60,-57]),seg([],4),seg([-57,-54])])[-1][\"away_max\"][4]==3
print(\"same-command empty skip: PASS\")
for day in (\"20261101\",\"20261106\"):
 old=json.loads(open(\"docs/diag_joint_hold_\"+day+\".json\").read())
 for stem,expected in old[\"runs\"].items():
  if day==\"20261101\" and not stem.startswith(\"pi05_20261101\"): continue
  got,_=d.summarize_run(d.RUNS_ROOT/stem)
  diff=[k for k,v in expected.items() if got.get(k)"'!=v]
  print(stem,"different_keys",diff,"events",got.get("event_segments"),"j4away",got.get("away_max",{}).get("j4"))
'"'" in /Users/shimazueiichi/r2r2r-scripts
 succeeded in 796ms:
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-Ub5xc75Q' (errno=Operation not permitted)
python3: warning: confstr() failed with code 5: couldn't get path of DARWIN_USER_TEMP_DIR; using /tmp instead
python3: error: couldn't create cache file '/tmp/xcrun_db-5GrM5h7w' (errno=Operation not permitted)
test_carry_cut_by_segments_without_samples (test_diag_joint_hold.TestPadAndFast)
codex R2 必須: 実測のない区間 (指令が変わる・すぐ返らない・長押し・組にできない) を挟めば引き継がない. ... ok
test_halt_one_to_one (test_diag_joint_hold.TestPadAndFast)
codex R1 推奨: stop はいちばん近い・まだ組にしていない halt と組む (同じ halt を 2 回使わない). ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.004s

OK
same-command empty skip: PASS
pi05_20261101_190347 different_keys [] events 0 j4away 0.0
pi05_20261101_190854 different_keys [] events 0 j4away 0.0
pi05_20261101_191547 different_keys [] events 0 j4away 0.0
pi05_20261101_192101 different_keys [] events 0 j4away 0.0
pi05_20261101_192418 different_keys [] events 2 j4away 0.0
pi05_20261101_192758 different_keys [] events 2 j4away 0.0
pi05_20261101_193041 different_keys [] events 2 j4away 0.0
pi05_20261101_193411 different_keys [] events 3 j4away 0.0
pi05_20261101_194042 different_keys [] events 4 j4away 0.0
pi05_20261106_183926 different_keys [] events 0 j4away 1.23
pi05_20261106_184452 different_keys [] events 0 j4away 3.78
pi05_20261106_184650 different_keys [] events 0 j4away 2.73
pi05_20261106_184906 different_keys [] events 0 j4away 2.37
pi05_20261106_185554 different_keys [] events 5 j4away 5.8

codex
**R2の必須指摘は解消しています。新たな必須修正はありません。** テストの条件分離について推奨が2点あります。

| 項目 | 判定 | 確認結果・修正案 |
|---|---|---|
| 実測なし区間での引き継ぎ解除 | 必須：解消 | `analyze_segments` の解除判定が実測件数の判定より前に移っています。非fast・指令不明は全関節、指令変更・長押しは該当関節の引き継ぎを解除します。 |
| 解除後のaway計算 | 据え置き：確認済み | `np.fmin(carry_min, a[0])` により、解除した関節は区間先頭の実測から数え直します。爪だけの空区間では引き継ぎます。 |
| 長押しの回帰テスト | 推奨 | [追加テスト](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/test_diag_joint_hold.py:369) はj4の指令変更も同時に起きるため、`skip` による解除がなくても通ります。**同じ指令のまま、実測なし・skip=4を挟むケース**を加えると解除条件を独立して検証できます。現実装はこの条件もメモリ上の検証で成功しました。 |
| stopの一対一テスト | 推奨 | [追加テスト](https://pixelstomotors.com/Users/shimazueiichi/r2r2r-scripts/pi05/test_diag_joint_hold.py:392) は各stopに最も近いhaltが異なるため、使用済みhaltの再利用禁止までは検証できません。haltを1件、範囲内のstopを2件にし、2件目が `pad_unknown` になるケースを追加してください。実装自体の一対一管理は維持されています。 |
| 11-01の互換性 | 据え置き：再確認済み | 9本すべて、保存JSONの全結果項目と再計算が一致しました。 |
| 11-06の選別結果 | 据え置き：再確認済み | 5本すべて保存JSONと一致。採用4本は外れ0、184452のj4 away最大3.78°。185554は外れ5区間、j4 away最大5.8°です。 |
| 184650の範囲・1コマの動き・偽物のassets | 据え置き | 従来の推奨事項として残す扱いを維持します。 |

追加テスト2件はファイル出力部分をメモリ処理に置き換えて成功しました。全23件の通常実行は、一時ファイルを作るため行っていません。ファイル変更、実機・Pod・Hugging Faceの操作は行っていません。

## 反映
- 推奨 2 件（同じ指令のまま長押しの関節だけで切るテスト・halt 1 件に stop 2 件で 2 件目が pad_unknown になるテスト）を pi05/test_diag_joint_hold.py に足した（24 件 OK）
````


## 19:51〜23:04 corr2 から corr3 を1,500ステップ学習した（約3.1時間・約292円）

依頼97の中で、Claude Code が `./lab_train.sh --log Log22 --model corr3` を、依頼が終わっても止まらない形（nohup）で始めました。Pod を作ってから学習が始まるまで約5分、学習は 20:02 の step 0 から 22:58 の step 1500 まで約2時間56分でした。終わったあと、Hugging Face（AI のモデルとデータを置いて配るサービス。非公開の置き場所 `PixelstoMotors/pi05-mecharm-trim-corr-lora-v3`）と KIOXIA の `checkpoints/pi05_mecharm_trim_corr_lora_v3/1499` にモデルを置き、Pod を消すところまで lab_train.sh が自動で行っています。

| step | 0 | 100 | 300 | 500 | 750 | 1000 | 1250 | 1450 |
|---|---|---|---|---|---|---|---|---|
| loss（corr3・この回） | 0.0129 | 0.0048 | 0.0044 | 0.0041 | 0.0036 | 0.0034 | 0.0027 | 0.0030 |
| loss（corr2・11/1 の夜） | 0.0024 | 0.0038 | 0.0041 | 0.0037 | 0.0033 | 0.0029 | 0.0024 | 0.0026 |

loss は、AI が出した動きと、学習データの動きのずれの大きさです。corr2 の学習は始めが0.0024でしたが、corr3 は0.0129から始まりました。どちらも、ひとつ前のモデルの続きから学習しています。corr3 の始めが高いのは、corr2 がまだ見たことのない動き（ゲームパッドの長押しで肩を一気に持ち上げる動きと、daemon を直したあとの速い動き）が新しいデータに入っていたためと見ています（見込み。データごとの loss は測っていない）。50ステップで0.0066まで下がり、終わりは corr2 と同じくらいの0.0030でした。

GPU は NVIDIA A40（1時間0.59ドル）で、使った GPU のメモリは最大41,283MiB（46,068MiB のうち）、学習中の使用率は中央100%でした。Pod を借りていた時間は188分2秒で、約1.85ドル（約292円）です。

### lab_train の端末の記録 lab_train_20261106_195143.txt

#### lab/Log22/train/lab_train_20261106_195143.txt

```text
[LOG   ] ターミナルの出力を残します → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/lab_train_20261106_195143.txt
[LAB   ] 2026-11-06 19:51:43  学習  GPU=NVIDIA A40 (借りられなければ NVIDIA RTX A6000 NVIDIA L40S)
         --model corr3 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v3)  データ mecharm_redblock_box_teach_v1_trim2_corr_v5
         pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5  steps=1500  init=/workspace/ckpt/corr_v2_1499/params  上限 5 時間  記録 /Volumes/KIOXIA/r2r2r-data/lab/Log22/train

=== 0. 送るものと Mac の道具を確かめる  (19:51:43)
[DATA  ] tar 116993536 バイト  (/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5/mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar)
[PROV  ] 始める時点のコード (284dfd7) と入力 3 つの指紋を測った
[MAC   ] 終わるまでスリープしないようにした (caffeinate pid 7188). ふたは閉じない
[WARN  ] 電源につながっていない. バッテリーだとスリープを止めきれないので, 電源につないでください
    (1 秒)

=== 1. Pod を作る  (19:51:44)
[POD   ] NVIDIA A40 を借ります (空きがなければ 30 秒おきに 5 回まで)
[POD   ] 作った: id uirn5j6iy4xbyx  GPU=NVIDIA A40  1時間 0.59 ドル  (記録 /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/lab_pod.json)
[POD   ] 上限: 11-07 00:51 (5 時間) を過ぎたら, ログを持ち帰って消します
    (2 秒)

=== 2. Pod が立ち上がるまで待つ  (19:51:46)
[POD   ] SSH の IP とポートを API から受け取った (<POD_IP>:<SSH_PORT>)
[POD   ] 入れた: a4e3c12d84ee
NVIDIA A40, 46068 MiB
[WATCH ] Pod 側の見張りを置いた: 11-07 01:51 を過ぎたら Pod が自分を消す (Pod 用の鍵. Mac の鍵は送っていない)
    (37 秒)

=== 3a. スクリプトを送る  (19:52:23)
    (52 秒)

=== 3b. huggingface_hub を入れ, 鍵を渡す  (19:53:15)
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
[90mHint: The `hf-cli` skill is not installed. Run `hf skills add -g` to teach your AI agents how to use the `hf` CLI.[0m
[32m✓ Logged in[0m
  user: PixelstoMotors
    (10 秒)

=== 3c. 続きから学習するモデルを取る (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → /workspace/ckpt/corr_v2_1499)  (19:53:25)

Fetching 28 files:   0%|          | 0/28 [00:00<?, ?it/s]
Fetching 28 files:   4%|▎         | 1/28 [00:00<00:03,  8.75it/s]
Fetching 28 files:  32%|███▏      | 9/28 [00:00<00:00, 37.51it/s]
Fetching 28 files:  46%|████▋     | 13/28 [00:04<00:05,  2.51it/s]
Fetching 28 files:  54%|█████▎    | 15/28 [00:04<00:04,  2.82it/s]
Fetching 28 files:  61%|██████    | 17/28 [00:06<00:04,  2.22it/s]
Fetching 28 files:  64%|██████▍   | 18/28 [00:06<00:04,  2.20it/s]
Fetching 28 files:  68%|██████▊   | 19/28 [00:06<00:03,  2.28it/s]
Fetching 28 files:  71%|███████▏  | 20/28 [00:07<00:03,  2.64it/s]
Fetching 28 files:  75%|███████▌  | 21/28 [00:07<00:02,  2.88it/s]
Fetching 28 files:  89%|████████▉ | 25/28 [00:08<00:00,  3.81it/s]
Fetching 28 files:  93%|█████████▎| 26/28 [00:08<00:00,  3.00it/s]
Fetching 28 files:  96%|█████████▋| 27/28 [00:10<00:00,  1.83it/s][32m✓ Downloaded[0m
  path: /workspace/ckpt/corr_v2_1499

Fetching 28 files: 100%|██████████| 28/28 [00:14<00:00,  1.31s/it]
Fetching 28 files: 100%|██████████| 28/28 [00:14<00:00,  1.96it/s]
6.0G	/workspace/ckpt/corr_v2_1499
    (17 秒)

=== 3d. openpi を取る (215abfb)  (19:53:42)
215abfb docs(droid): fix config search instruction (#1023)
    (16 秒)

=== 3e. uv sync  (19:53:58)
downloading uv 0.13.7 x86_64-unknown-linux-gnu
WARN: The following commands are shadowed by other commands in your PATH: uv uvx
Using CPython 3.11.13 interpreter at: /usr/bin/python3.11
Creating virtual environment at: .venv
Resolved 279 packages in 4ms
   Building openpi-client @ file:///workspace/openpi/packages/openpi-client
   Building openpi @ file:///workspace/openpi
   Updating https://github.com/huggingface/lerobot (0cf864870cf29f4738d3ade893e6fd13fbd7cdb5)
Downloading opencv-python (60.1MiB)
Downloading nvidia-nvjitlink-cu12 (18.8MiB)
Downloading cmake (26.6MiB)
Downloading jax-cuda12-plugin (15.9MiB)
Downloading nvidia-cublas-cu12 (374.9MiB)
Downloading sympy (6.0MiB)
Downloading llvmlite (40.4MiB)
Downloading nvidia-cusolver-cu12 (150.9MiB)
Downloading nvidia-cuda-nvrtc-cu12 (22.6MiB)
Downloading pyarrow (40.3MiB)
Downloading nvidia-curand-cu12 (53.7MiB)
Downloading rerun-sdk (68.1MiB)
Downloading nvidia-cufft-cu12 (190.9MiB)
Downloading nvidia-cusparse-cu12 (206.5MiB)
Downloading nvidia-cudnn-cu12 (544.5MiB)
Downloading torch (783.1MiB)
Downloading hf-transfer (3.4MiB)
Downloading opencv-python-headless (47.7MiB)
Downloading scipy (35.9MiB)
Downloading lxml (4.7MiB)
Downloading polars (34.6MiB)
Downloading wandb (20.4MiB)
Downloading imageio-ffmpeg (28.1MiB)
Downloading pandas (12.5MiB)
Downloading nvidia-cuda-nvcc-cu12 (38.7MiB)
Downloading h5py (4.3MiB)
Downloading triton (148.5MiB)
Downloading jax-cuda12-pjrt (99.8MiB)
Downloading numcodecs (8.4MiB)
Downloading jaxlib (100.2MiB)
Downloading nvidia-cusparselt-cu12 (149.5MiB)
Downloading labmaze (4.7MiB)
Downloading diffusers (3.4MiB)
Downloading nvidia-nccl-cu12 (192.0MiB)
Downloading mujoco (4.4MiB)
Downloading pyopengl (3.0MiB)
Downloading tensorstore (17.5MiB)
Downloading nvidia-cuda-cupti-cu12 (8.5MiB)
Downloading ruff (11.0MiB)
Downloading av (34.7MiB)
Downloading dm-control (37.5MiB)
Downloading virtualenv (5.8MiB)
Downloading torchvision (7.1MiB)
Downloading hf-xet (4.9MiB)
Downloading numba (3.7MiB)
Downloading transformers (10.3MiB)
Downloading pillow (4.4MiB)
Downloading numpy (17.4MiB)
Downloading matplotlib (8.2MiB)
Downloading fonttools (4.7MiB)
 Downloaded pyopengl
 Downloaded diffusers
Downloading debugpy (3.0MiB)
Downloading tokenizers (2.9MiB)
 Downloaded hf-transfer
Downloading jax (2.3MiB)
 Downloaded numba
Downloading widgetsnbextension (2.1MiB)
 Downloaded h5py
Downloading ml-dtypes (2.1MiB)
 Downloaded mujoco
 Downloaded pillow
 Downloaded labmaze
Downloading networkx (1.9MiB)
Downloading pydantic-core (1.9MiB)
Downloading aiohttp (1.6MiB)
 Downloaded lxml
 Downloaded fonttools
Downloading kiwisolver (1.4MiB)
Downloading jedi (1.5MiB)
 Downloaded hf-xet
Downloading torchcodec (1.3MiB)
 Downloaded virtualenv
Downloading sentencepiece (1.2MiB)
 Downloaded widgetsnbextension
 Downloaded jax
 Downloaded sympy
Downloading pygments (1.2MiB)
Downloading setuptools (1.1MiB)
Downloading nvidia-cufile-cu12 (1.1MiB)
 Downloaded kiwisolver
 Downloaded tokenizers
 Downloaded debugpy
 Downloaded aiohttp
 Downloaded jedi
Downloading pymunk (1.0MiB)
 Downloaded ml-dtypes
 Downloaded pydantic-core
 Downloaded torchcodec
 Downloaded networkx
 Downloaded torchvision
 Downloaded sentencepiece
 Downloaded nvidia-cufile-cu12
 Downloaded pygments
 Downloaded setuptools
 Downloaded pymunk
 Downloaded matplotlib
 Downloaded numcodecs
 Downloaded nvidia-cuda-cupti-cu12
   Building evdev==1.9.2
   Building antlr4-python3-runtime==4.9.3
 Downloaded transformers
 Downloaded ruff
 Downloaded pandas
 Downloaded jax-cuda12-plugin
 Downloaded numpy
 Downloaded tensorstore
      Built openpi @ file:///workspace/openpi
      Built openpi-client @ file:///workspace/openpi/packages/openpi-client
 Downloaded nvidia-nvjitlink-cu12
 Downloaded wandb
 Downloaded nvidia-cuda-nvrtc-cu12
      Built antlr4-python3-runtime==4.9.3
 Downloaded imageio-ffmpeg
 Downloaded cmake
 Downloaded polars
 Downloaded av
 Downloaded scipy
 Downloaded dm-control
 Downloaded nvidia-cuda-nvcc-cu12
 Downloaded pyarrow
 Downloaded llvmlite
 Downloaded opencv-python-headless
 Downloaded nvidia-curand-cu12
      Built evdev==1.9.2
 Downloaded opencv-python
    Updated https://github.com/huggingface/lerobot (0cf864870cf29f4738d3ade893e6fd13fbd7cdb5)
   Building lerobot @ git+https://github.com/huggingface/lerobot@0cf864870cf29f4738d3ade893e6fd13fbd7cdb5
 Downloaded rerun-sdk
      Built lerobot @ git+https://github.com/huggingface/lerobot@0cf864870cf29f4738d3ade893e6fd13fbd7cdb5
 Downloaded jaxlib
 Downloaded jax-cuda12-pjrt
 Downloaded triton
 Downloaded nvidia-cusolver-cu12
 Downloaded nvidia-cusparselt-cu12
 Downloaded nvidia-cufft-cu12
 Downloaded nvidia-nccl-cu12
 Downloaded nvidia-cusparse-cu12
 Downloaded nvidia-cublas-cu12
 Downloaded nvidia-cudnn-cu12
 Downloaded torch
Prepared 240 packages in 14.67s
Installed 240 packages in 1.86s
 + absl-py==2.3.0
 + aiohappyeyeballs==2.6.1
 + aiohttp==3.12.4
 + aiosignal==1.3.2
 + annotated-types==0.7.0
 + antlr4-python3-runtime==4.9.3
 + asttokens==3.0.0
 + attrs==25.3.0
 + augmax==0.4.1
 + av==17.0.0
 + beartype==0.19.0
 + beautifulsoup4==4.13.4
 + blinker==1.9.0
 + cachetools==5.5.2
 + certifi==2025.4.26
 + cffi==1.17.1
 + cfgv==3.4.0
 + charset-normalizer==3.4.2
 + chex==0.1.90
 + click==8.2.1
 + cloudpickle==3.1.1
 + cmake==4.0.2
 + comm==0.2.2
 + contourpy==1.3.2
 + crc32c==2.7.1
 + cycler==0.12.1
 + datasets==3.6.0
 + debugpy==1.8.14
 + decorator==5.2.1
 + deepdiff==8.5.0
 + diffusers==0.33.1
 + dill==0.3.8
 + distlib==0.3.9
 + dm-control==1.0.14
 + dm-env==1.6
 + dm-tree==0.1.9
 + docker-pycreds==0.4.0
 + docstring-parser==0.16
 + donfig==0.8.1.post1
 + draccus==0.10.0
 + einops==0.8.1
 + equinox==0.12.2
 + etils==1.12.2
 + evdev==1.9.2
 + executing==2.2.0
 + farama-notifications==0.0.4
 + filelock==3.18.0
 + flask==3.1.1
 + flatbuffers==25.2.10
 + flax==0.10.2
 + fonttools==4.58.1
 + frozenlist==1.6.0
 + fsspec==2025.3.0
 + gcsfs==2025.3.0
 + gdown==5.2.0
 + gitdb==4.0.12
 + gitpython==3.1.44
 + glfw==2.9.0
 + google-api-core==2.24.2
 + google-auth==2.40.2
 + google-auth-oauthlib==1.2.2
 + google-cloud-core==2.4.3
 + google-cloud-storage==3.1.0
 + google-crc32c==1.7.1
 + google-resumable-media==2.7.2
 + googleapis-common-protos==1.70.0
 + gym-aloha==0.1.1
 + gymnasium==0.29.1
 + h5py==3.13.0
 + hf-transfer==0.1.9
 + hf-xet==1.1.2
 + huggingface-hub==0.32.3
 + humanize==4.12.3
 + identify==2.6.12
 + idna==3.10
 + imageio==2.37.0
 + imageio-ffmpeg==0.6.0
 + importlib-metadata==8.7.0
 + importlib-resources==6.5.2
 + iniconfig==2.1.0
 + inquirerpy==0.3.4
 + ipykernel==6.29.5
 + ipython==9.2.0
 + ipython-pygments-lexers==1.1.1
 + ipywidgets==8.1.7
 + itsdangerous==2.2.0
 + jax==0.5.3
 + jax-cuda12-pjrt==0.5.3
 + jax-cuda12-plugin==0.5.3
 + jaxlib==0.5.3
 + jaxtyping==0.2.36
 + jedi==0.19.2
 + jinja2==3.1.6
 + jsonlines==4.0.0
 + jupyter-client==8.6.3
 + jupyter-core==5.8.1
 + jupyterlab-widgets==3.0.15
 + kiwisolver==1.4.8
 + labmaze==1.0.6
 + lerobot==0.1.0 (from git+https://github.com/huggingface/lerobot@0cf864870cf29f4738d3ade893e6fd13fbd7cdb5)
 + llvmlite==0.44.0
 + lxml==5.4.0
 + markdown-it-py==3.0.0
 + markupsafe==3.0.2
 + matplotlib==3.10.3
 + matplotlib-inline==0.1.7
 + mdurl==0.1.2
 + mergedeep==1.3.4
 + ml-collections==1.0.0
 + ml-dtypes==0.4.1
 + mpmath==1.3.0
 + msgpack==1.1.0
 + mujoco==2.3.7
 + multidict==6.4.4
 + multiprocess==0.70.16
 + mypy-extensions==1.1.0
 + nest-asyncio==1.6.0
 + networkx==3.5
 + nodeenv==1.9.1
 + numba==0.61.2
 + numcodecs==0.16.1
 + numpy==1.26.4
 + numpydantic==1.6.9
 + nvidia-cublas-cu12==12.6.4.1
 + nvidia-cuda-cupti-cu12==12.6.80
 + nvidia-cuda-nvcc-cu12==12.9.41
 + nvidia-cuda-nvrtc-cu12==12.6.77
 + nvidia-cuda-runtime-cu12==12.6.77
 + nvidia-cudnn-cu12==9.5.1.17
 + nvidia-cufft-cu12==11.3.0.4
 + nvidia-cufile-cu12==1.11.1.6
 + nvidia-curand-cu12==10.3.7.77
 + nvidia-cusolver-cu12==11.7.1.2
 + nvidia-cusparse-cu12==12.5.4.2
 + nvidia-cusparselt-cu12==0.6.3
 + nvidia-ml-py==12.575.51
 + nvidia-nccl-cu12==2.26.2
 + nvidia-nvjitlink-cu12==12.6.85
 + nvidia-nvtx-cu12==12.6.77
 + oauthlib==3.2.2
 + omegaconf==2.3.0
 + opencv-python==4.11.0.86
 + opencv-python-headless==4.11.0.86
 + openpi==0.1.0 (from file:///workspace/openpi)
 + openpi-client==0.1.0 (from file:///workspace/openpi/packages/openpi-client)
 + opt-einsum==3.4.0
 + optax==0.2.4
 + orbax-checkpoint==0.11.13
 + orderly-set==5.4.1
 + packaging==25.0
 + pandas==2.2.3
 + parso==0.8.4
 + pexpect==4.9.0
 + pfzy==0.3.4
 + pillow==11.2.1
 + platformdirs==4.3.8
 + pluggy==1.6.0
 + polars==1.30.0
 + pre-commit==4.2.0
 + prompt-toolkit==3.0.51
 + propcache==0.3.1
 + proto-plus==1.26.1
 + protobuf==4.25.8
 + psutil==7.0.0
 + ptyprocess==0.7.0
 + pure-eval==0.2.3
 + pyarrow==20.0.0
 + pyasn1==0.6.1
 + pyasn1-modules==0.4.2
 + pycparser==2.22
 + pydantic==2.11.5
 + pydantic-core==2.33.2
 + pygments==2.19.1
 + pymunk==7.0.0
 + pynput==1.8.1
 + pynvml==12.0.0
 + pyopengl==3.1.9
 + pyparsing==3.2.3
 + pysocks==1.7.1
 + pytest==8.3.5
 + python-dateutil==2.9.0.post0
 + python-xlib==0.33
 + pytz==2025.2
 + pyyaml==6.0.2
 + pyyaml-include==1.4.1
 + pyzmq==26.4.0
 + regex==2024.11.6
 + requests==2.32.3
 + requests-oauthlib==2.0.0
 + rerun-sdk==0.23.1
 + rich==14.0.0
 + rsa==4.9.1
 + ruff==0.11.12
 + safetensors==0.5.3
 + scipy==1.15.3
 + sentencepiece==0.2.0
 + sentry-sdk==2.29.1
 + setproctitle==1.3.6
 + setuptools==80.9.0
 + shtab==1.7.2
 + simplejson==3.20.1
 + six==1.17.0
 + smmap==5.0.2
 + soupsieve==2.7
 + stack-data==0.6.3
 + sympy==1.14.0
 + tensorstore==0.1.74
 + termcolor==3.1.0
 + tokenizers==0.21.1
 + toml==0.10.2
 + toolz==1.0.0
 + torch==2.7.1
 + torchcodec==0.4.0
 + torchvision==0.22.1
 + tornado==6.5.1
 + tqdm==4.67.1
 + tqdm-loggable==0.2
 + traitlets==5.14.3
 + transformers==4.53.2
 + treescope==0.1.9
 + triton==3.3.1
 + typeguard==4.4.2
 + typing-extensions==4.13.2
 + typing-inspect==0.9.0
 + typing-inspection==0.4.1
 + tyro==0.9.22
 + tzdata==2025.2
 + urllib3==2.4.0
 + virtualenv==20.31.2
 + wadler-lindig==0.1.6
 + wandb==0.19.11
 + wcwidth==0.2.13
 + websockets==15.0.1
 + werkzeug==3.1.3
 + widgetsnbextension==4.0.14
 + wrapt==1.14.1
 + xxhash==3.5.0
 + yarl==1.20.0
 + zarr==3.0.8
 + zipp==3.22.0
    (20 秒)

=== 3f. addon・設定・assets  (19:54:18)
installed: policies/mecharm_policy.py + config.py (pi05_mecharm_lora)
installed: pi05_mecharm_trim_lora (repo_id=local/mecharm_redblock_box_teach_v1_trim2, 4,000 steps)
[PROV  ] openpi 215abfb217dbac7d5f1273282331b9b1866c0479 (2 ファイル変更・addon と設定を入れたあと)
    (7 秒)

=== 4a. tar を送る  (19:54:25)
-rwx------ 1 root root 116993536 Nov  6 10:56 /workspace/data/mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar
    (103 秒)

=== 4b. pod_prep_trim_corr.sh 1500 /workspace/ckpt/corr_v2_1499/params v5  (19:56:08)
== 1. tar を広げる → /workspace/data/mecharm_redblock_box_teach_v1_trim2_corr_v5
   エピソード 30 本 (Mac で作ったときは 30 本)
== 2. 学習設定 pi05_mecharm_trim_corr_v5_lora を足す (steps=1500 init=/workspace/ckpt/corr_v2_1499/params)
installed: pi05_mecharm_trim_corr_v5_lora (repo_id=local/mecharm_redblock_box_teach_v1_trim2_corr_v5, 1500 steps, init=/workspace/ckpt/corr_v2_1499/params)
== 3. LeRobot 形式に変換 (local/mecharm_redblock_box_teach_v1_trim2_corr_v5)

Map:   0%|          | 0/118 [00:00<?, ? examples/s]
Map:  84%|████████▍ | 99/118 [00:00<00:00, 972.36 examples/s]
Map: 100%|██████████| 118/118 [00:00<00:00, 846.40 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 90.30ba/s]
  pi05_20261031_175527: 118 steps

Map:   0%|          | 0/152 [00:00<?, ? examples/s]
Map:  85%|████████▍ | 129/152 [00:00<00:00, 1271.54 examples/s]
Map: 100%|██████████| 152/152 [00:00<00:00, 1084.97 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 89.94ba/s]
  pi05_20261031_180435: 152 steps

Map:   0%|          | 0/126 [00:00<?, ? examples/s]
Map: 100%|██████████| 126/126 [00:00<00:00, 1098.62 examples/s]
Map: 100%|██████████| 126/126 [00:00<00:00, 1070.64 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 83.71ba/s]
  pi05_20261031_181946: 126 steps

Map:   0%|          | 0/133 [00:00<?, ? examples/s]
Map: 100%|██████████| 133/133 [00:00<00:00, 1219.33 examples/s]
Map: 100%|██████████| 133/133 [00:00<00:00, 1191.78 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 114.57ba/s]
  pi05_20261031_184016: 133 steps

Map:   0%|          | 0/115 [00:00<?, ? examples/s]
Map: 100%|██████████| 115/115 [00:00<00:00, 1259.43 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 88.77ba/s]
  pi05_20261101_190347: 115 steps

Map:   0%|          | 0/145 [00:00<?, ? examples/s]
Map:  90%|████████▉ | 130/145 [00:00<00:00, 1280.71 examples/s]
Map: 100%|██████████| 145/145 [00:00<00:00, 1096.39 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 77.33ba/s]
  pi05_20261101_190854: 145 steps

Map:   0%|          | 0/104 [00:00<?, ? examples/s]
Map: 100%|██████████| 104/104 [00:00<00:00, 1184.46 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 100.10ba/s]
  pi05_20261101_192101: 104 steps

Map:   0%|          | 0/188 [00:00<?, ? examples/s]
Map:  49%|████▉     | 92/188 [00:00<00:00, 898.09 examples/s]
Map: 100%|██████████| 188/188 [00:00<00:00, 933.81 examples/s]
Map: 100%|██████████| 188/188 [00:00<00:00, 915.68 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 76.15ba/s]
  pi05_20261106_183926: 188 steps

Map:   0%|          | 0/190 [00:00<?, ? examples/s]
Map:  65%|██████▌   | 124/190 [00:00<00:00, 1223.93 examples/s]
Map: 100%|██████████| 190/190 [00:00<00:00, 1116.45 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 82.95ba/s]
  pi05_20261106_184452: 190 steps

Map:   0%|          | 0/178 [00:00<?, ? examples/s]
Map:  77%|███████▋  | 137/178 [00:00<00:00, 1355.49 examples/s]
Map: 100%|██████████| 178/178 [00:00<00:00, 1201.71 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 105.06ba/s]
  pi05_20261106_184650: 178 steps

Map:   0%|          | 0/135 [00:00<?, ? examples/s]
Map: 100%|██████████| 135/135 [00:00<00:00, 1279.14 examples/s]
Map: 100%|██████████| 135/135 [00:00<00:00, 1249.93 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 92.10ba/s]
  pi05_20261106_184906: 135 steps

Map:   0%|          | 0/144 [00:00<?, ? examples/s]
Map:  98%|█████████▊| 141/144 [00:00<00:00, 1397.81 examples/s]
Map: 100%|██████████| 144/144 [00:00<00:00, 1260.94 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 88.36ba/s]
  run_0001_20261019_183454: 144 steps

Map:   0%|          | 0/147 [00:00<?, ? examples/s]
Map:  67%|██████▋   | 98/147 [00:00<00:00, 968.23 examples/s]
Map: 100%|██████████| 147/147 [00:00<00:00, 863.83 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 55.79ba/s]
  run_0001_20261019_191257: 147 steps

Map:   0%|          | 0/144 [00:00<?, ? examples/s]
Map:  95%|█████████▌| 137/144 [00:00<00:00, 1355.84 examples/s]
Map: 100%|██████████| 144/144 [00:00<00:00, 1175.39 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 79.65ba/s]
  run_0001_20261019_191347: 144 steps

Map:   0%|          | 0/163 [00:00<?, ? examples/s]
Map:  72%|███████▏  | 118/163 [00:00<00:00, 1163.69 examples/s]
Map: 100%|██████████| 163/163 [00:00<00:00, 956.02 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 61.18ba/s]
  run_0001_20261019_191441: 163 steps

Map:   0%|          | 0/139 [00:00<?, ? examples/s]
Map:  73%|███████▎  | 102/139 [00:00<00:00, 1005.16 examples/s]
Map: 100%|██████████| 139/139 [00:00<00:00, 864.78 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 73.55ba/s]
  run_0001_20261019_191541: 139 steps

Map:   0%|          | 0/138 [00:00<?, ? examples/s]
Map:  76%|███████▌  | 105/138 [00:00<00:00, 1039.93 examples/s]
Map: 100%|██████████| 138/138 [00:00<00:00, 972.58 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 84.37ba/s]
  run_0001_20261019_191635: 138 steps

Map:   0%|          | 0/140 [00:00<?, ? examples/s]
Map:  97%|█████████▋| 136/140 [00:00<00:00, 1347.88 examples/s]
Map: 100%|██████████| 140/140 [00:00<00:00, 1168.10 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 96.59ba/s]
  run_0001_20261019_192343: 140 steps

Map:   0%|          | 0/138 [00:00<?, ? examples/s]
Map:  95%|█████████▍| 131/138 [00:00<00:00, 1302.34 examples/s]
Map: 100%|██████████| 138/138 [00:00<00:00, 1118.87 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 114.55ba/s]
  run_0001_20261019_192438: 138 steps

Map:   0%|          | 0/140 [00:00<?, ? examples/s]
Map:  99%|█████████▊| 138/140 [00:00<00:00, 1361.52 examples/s]
Map: 100%|██████████| 140/140 [00:00<00:00, 1194.44 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 76.66ba/s]
  run_0001_20261019_192539: 140 steps

Map:   0%|          | 0/137 [00:00<?, ? examples/s]
Map:  85%|████████▌ | 117/137 [00:00<00:00, 1149.13 examples/s]
Map: 100%|██████████| 137/137 [00:00<00:00, 1042.42 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 78.92ba/s]
  run_0001_20261019_192633: 137 steps

Map:   0%|          | 0/164 [00:00<?, ? examples/s]
Map:  58%|█████▊    | 95/164 [00:00<00:00, 926.76 examples/s]
Map: 100%|██████████| 164/164 [00:00<00:00, 803.84 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 50.97ba/s]
  run_0001_20261019_193404: 164 steps

Map:   0%|          | 0/159 [00:00<?, ? examples/s]
Map:  67%|██████▋   | 107/159 [00:00<00:00, 1055.78 examples/s]
Map: 100%|██████████| 159/159 [00:00<00:00, 945.11 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 57.83ba/s]
  run_0001_20261019_193459: 159 steps

Map:   0%|          | 0/156 [00:00<?, ? examples/s]
Map:  76%|███████▌  | 118/156 [00:00<00:00, 1163.21 examples/s]
Map: 100%|██████████| 156/156 [00:00<00:00, 1098.09 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 117.17ba/s]
  run_0001_20261019_193617: 156 steps

Map:   0%|          | 0/165 [00:00<?, ? examples/s]
Map:  79%|███████▉  | 130/165 [00:00<00:00, 1279.45 examples/s]
Map: 100%|██████████| 165/165 [00:00<00:00, 1118.68 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 91.08ba/s]
  run_0001_20261019_193825: 165 steps

Map:   0%|          | 0/151 [00:00<?, ? examples/s]
Map:  89%|████████▉ | 135/151 [00:00<00:00, 1339.38 examples/s]
Map: 100%|██████████| 151/151 [00:00<00:00, 1141.87 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 57.94ba/s]
  run_0001_20261019_193932: 151 steps

Map:   0%|          | 0/123 [00:00<?, ? examples/s]
Map: 100%|██████████| 123/123 [00:00<00:00, 1060.43 examples/s]
Map: 100%|██████████| 123/123 [00:00<00:00, 1034.37 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 77.36ba/s]
  run_0001_20261019_194818: 123 steps

Map:   0%|          | 0/126 [00:00<?, ? examples/s]
Map:  92%|█████████▏| 116/126 [00:00<00:00, 1150.81 examples/s]
Map: 100%|██████████| 126/126 [00:00<00:00, 1008.68 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 74.45ba/s]
  run_0001_20261019_194913: 126 steps

Map:   0%|          | 0/123 [00:00<?, ? examples/s]
Map:  97%|█████████▋| 119/123 [00:00<00:00, 1174.84 examples/s]
Map: 100%|██████████| 123/123 [00:00<00:00, 999.16 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 83.82ba/s]
  run_0001_20261019_195023: 123 steps

Map:   0%|          | 0/120 [00:00<?, ? examples/s]
Map: 100%|██████████| 120/120 [00:00<00:00, 1110.76 examples/s]
Map: 100%|██████████| 120/120 [00:00<00:00, 1085.65 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 94.67ba/s]
  run_0001_20261019_195120: 120 steps
done: 30 episodes / 4301 frames → /root/.cache/huggingface/lerobot/local/mecharm_redblock_box_teach_v1_trim2_corr_v5
== 4. 正規化の値は trim2 の値をそのまま置く (計算し直さない)
   /workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json → /workspace/openpi/assets/pi05_mecharm_trim_corr_v5_lora/local/mecharm_redblock_box_teach_v1_trim2_corr_v5/norm_stats.json (sha256 80346837eb12…)
== 5. 準備できました. 学習はこのコマンドで始める (このスクリプトでは流さない):
   cd /workspace/openpi && XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 uv run scripts/train.py pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5 --overwrite 2>&1 | tee /workspace/logs/train_trim_corr_v5.log
    (48 秒)

=== 5. 学習を始める  (19:56:56)
[TRAIN ] 学習を始めました (Pod の中で setsid nohup. Mac とのつながりが切れても続く)
         ログ /workspace/logs/train_trim_corr_v5.log  GPU /workspace/logs/gpu_train.csv (30 秒おき)
    (6 秒)

=== 6. 学習を見守る (5 分おき)  (19:57:02)
[TRAIN ] 19:57  step -/1500  loss -  経過 0:00  上限まで 4:54

========================================================================
 学習は Pod の中で回っています. ここまで来れば寝てかまいません.
 このターミナルは閉じない・Mac のふたは閉じない (電源につないだまま).
 終わると通知が出ます. まとめは /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/result_*.txt
========================================================================

[TRAIN ] 20:02  step 0/1500  loss 0.0129 (Step 0 の行)  経過 0:05  上限まで 4:49
[TRAIN ] 20:07  step 44/1500  loss 0.0129 (Step 0 の行)  経過 0:10  上限まで 4:44
[TRAIN ] 20:12  step 88/1500  loss 0.0066 (Step 50 の行)  経過 0:15  上限まで 4:39
[TRAIN ] 20:17  step 130/1500  loss 0.0048 (Step 100 の行)  経過 0:20  上限まで 4:34
[TRAIN ] 20:22  step 174/1500  loss 0.0051 (Step 150 の行)  経過 0:25  上限まで 4:29
[TRAIN ] 20:27  step 216/1500  loss 0.0052 (Step 200 の行)  経過 0:30  上限まで 4:24
[TRAIN ] 20:32  step 260/1500  loss 0.0045 (Step 250 の行)  経過 0:35  上限まで 4:19
[TRAIN ] 20:37  step 300/1500  loss 0.0044 (Step 300 の行)  経過 0:40  上限まで 4:14
[TRAIN ] 20:42  step 346/1500  loss 0.0044 (Step 300 の行)  経過 0:45  上限まで 4:09
[TRAIN ] 20:47  step 388/1500  loss 0.0042 (Step 350 の行)  経過 0:50  上限まで 4:04
[TRAIN ] 20:52  step 430/1500  loss 0.0039 (Step 400 の行)  経過 0:55  上限まで 3:59
[TRAIN ] 20:57  step 474/1500  loss 0.0043 (Step 450 の行)  経過 1:00  上限まで 3:54
[TRAIN ] 21:02  step 516/1500  loss 0.0041 (Step 500 の行)  経過 1:05  上限まで 3:49
[TRAIN ] 21:07  step 560/1500  loss 0.0043 (Step 550 の行)  経過 1:10  上限まで 3:44
[TRAIN ] 21:12  step 600/1500  loss 0.0040 (Step 600 の行)  経過 1:15  上限まで 3:38
[TRAIN ] 21:17  step 646/1500  loss 0.0040 (Step 600 の行)  経過 1:20  上限まで 3:33
[TRAIN ] 21:22  step 688/1500  loss 0.0039 (Step 650 の行)  経過 1:25  上限まで 3:28
[TRAIN ] 21:27  step 732/1500  loss 0.0039 (Step 700 の行)  経過 1:30  上限まで 3:23
[TRAIN ] 21:33  step 774/1500  loss 0.0036 (Step 750 の行)  経過 1:35  上限まで 3:18
[TRAIN ] 21:38  step 818/1500  loss 0.0034 (Step 800 の行)  経過 1:41  上限まで 3:13
[TRAIN ] 21:43  step 860/1500  loss 0.0033 (Step 850 の行)  経過 1:46  上限まで 3:08
[TRAIN ] 21:48  step 904/1500  loss 0.0035 (Step 900 の行)  経過 1:51  上限まで 3:03
[TRAIN ] 21:53  step 948/1500  loss 0.0035 (Step 900 の行)  経過 1:56  上限まで 2:58
[TRAIN ] 21:58  step 990/1500  loss 0.0033 (Step 950 の行)  経過 2:01  上限まで 2:53
[TRAIN ] 22:03  step 1030/1500  loss 0.0034 (Step 1000 の行)  経過 2:06  上限まで 2:48
[TRAIN ] 22:08  step 1080/1500  loss 0.0028 (Step 1050 の行)  経過 2:11  上限まで 2:43
[TRAIN ] 22:13  step 1120/1500  loss 0.0037 (Step 1100 の行)  経過 2:16  上限まで 2:38
[TRAIN ] 22:18  step 1160/1500  loss 0.0029 (Step 1150 の行)  経過 2:21  上限まで 2:33
[TRAIN ] 22:23  step 1200/1500  loss 0.0033 (Step 1200 の行)  経過 2:26  上限まで 2:28
[TRAIN ] 22:28  step 1250/1500  loss 0.0033 (Step 1200 の行)  経過 2:31  上限まで 2:23
[TRAIN ] 22:33  step 1290/1500  loss 0.0027 (Step 1250 の行)  経過 2:36  上限まで 2:18
[TRAIN ] 22:38  step 1330/1500  loss 0.0025 (Step 1300 の行)  経過 2:41  上限まで 2:12
[TRAIN ] 22:43  step 1380/1500  loss 0.0031 (Step 1350 の行)  経過 2:46  上限まで 2:07
[TRAIN ] 22:48  step 1420/1500  loss 0.0028 (Step 1400 の行)  経過 2:51  上限まで 2:02
[TRAIN ] 22:53  step 1460/1500  loss 0.0030 (Step 1450 の行)  経過 2:56  上限まで 1:57
[TRAIN ] 22:58  step 1500/1500  loss 0.0030 (Step 1450 の行)  経過 3:01  上限まで 1:52
    (10917 秒)

[TRAIN ] 学習が終わりました (最後のチェックポイント 1499, loss 0.0030)
[WATCH ] Pod 側の見張りを 01:58 まで延ばした (上げる・持ち帰る間に消えないように)

=== 7a. Pod 側の一覧 (1499)  (22:59:02)
[LIST  ] 35 ファイル 6335848626 バイト (train_state なし) → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/pod_files_1499.tsv
    (2 秒)

=== 7b. Hugging Face へ上げる (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3・非公開)  (22:59:04)
[HF    ] 上げるもの: 35 ファイル 6,335,848,626 バイト (train_state/ は上げない)
HFUP ok PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 に 35 ファイル 6,335,848,626 バイトを上げ, 名前・バイト数が全部合うのを確かめた
    (14 秒)

=== 7d. ログを持ち帰る  (22:59:18)
[LOG   ] train_trim_corr_v5.log を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/train_trim_corr_v5.log (981 行)
[LOG   ] gpu_train.csv を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/gpu_train.csv (367 行)
[LOG   ] train_run.log を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/train_run.log (2 行)
[LOG   ] prep_trim_corr.log を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/prep_trim_corr.log (100 行)
[LOG   ] watchdog.log は Pod にない (持ち帰れなかった)
[LOG   ] Pod 側のチェックポイントの一覧 → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/pod_checkpoints.tsv (122 ファイル)
    (28 秒)

=== Pod を消す (id uirn5j6iy4xbyx)  (22:59:46)
[STOP  ] 削除 (Terminate) を送った (22:59:48)
[STOP  ] Pod (id uirn5j6iy4xbyx) が消えたことを API で確かめた
[COST  ] 借りていた時間 188 分 2 秒 (3.13 時間) × 1時間 0.59 ドル = 約 1.85 ドル (約 292 円. 1ドル=158円)  GPU=NVIDIA A40

=== 7e. KIOXIA へ Hugging Face から取る  (22:59:48)
[KIOXIA] Hugging Face (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3) から取ります → /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/.incoming_1499_20261106_225949_7138
Still waiting to acquire lock on /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/.incoming_1499_20261106_225949_7138/.cache/huggingface/.gitignore.lock (elapsed: 0.1 seconds)
Still waiting to acquire lock on /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/.incoming_1499_20261106_225949_7138/.cache/huggingface/.gitignore.lock (elapsed: 0.1 seconds)
path=/Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/.incoming_1499_20261106_225949_7138
[KIOXIA] /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 に置いた (35 ファイル 6335848626 バイト. Pod 側と名前・バイト数が全部合った)
    (254 秒)

=== 由来 (train_provenance.json) を書く
[PROV  ] /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/train_provenance.json
[PROV  ] /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/1499/train_provenance.json
[PROV  ] hf: url=https://huggingface.co/PixelstoMotors/pi05-mecharm-trim-corr-lora-v3/commit/b8a5ccbc68c8ac6dadf30a4d074ee66bfadee9c9
[PROV  ] 由来: /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/train_provenance.json ・ /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/1499/train_provenance.json ・ Hugging Face (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3) にも置いた

=== かかった時間
  0. 送るものと Mac の道具を確かめる      1 秒
  1. Pod を作る                          2 秒
  2. Pod が立ち上がるまで待つ     37 秒
  3a. スクリプトを送る             52 秒
  3b. huggingface_hub を入れ, 鍵を渡す     10 秒
  3c. 続きから学習するモデルを取る (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → /workspace/ckpt/corr_v2_1499)     17 秒
  3d. openpi を取る (215abfb)           16 秒
  3e. uv sync                              20 秒
  3f. addon・設定・assets               7 秒
  4a. tar を送る                       103 秒
  4b. pod_prep_trim_corr.sh 1500 /workspace/ckpt/corr_v2_1499/params v5     48 秒
  5. 学習を始める                     6 秒
  6. 学習を見守る (5 分おき)   10917 秒
  7a. Pod 側の一覧 (1499)               2 秒
  7b. Hugging Face へ上げる (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3・非公開)     14 秒
  7d. ログを持ち帰る                28 秒
  7e. KIOXIA へ Hugging Face から取る    254 秒

[RESULT] 成功: 学習が終わりました (step 1499, loss 0.0030). Hugging Face と KIOXIA に置き, Pod を消しました. 188 分 2 秒 (3.13 時間) × 1時間 0.59 ドル = 約 1.85 ドル (約 292 円. 1ドル=158円)
[RESULT] まとめ → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/result_20261106_195143.txt
```

### 学習のまとめ result_20261106_195143.txt

#### lab/Log22/train/result_20261106_195143.txt

```text
lab_train.sh Log22  2026-11-06 23:04:05
結果: 成功: 学習が終わりました (step 1499, loss 0.0030). Hugging Face と KIOXIA に置き, Pod を消しました. 188 分 2 秒 (3.13 時間) × 1時間 0.59 ドル = 約 1.85 ドル (約 292 円. 1ドル=158円)
学習: pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5  最後のチェックポイント 1499  最後の loss 0.0030 (Step 1450)
Hugging Face (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3): ok PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 に 35 ファイル 6,335,848,626 バイトを上げ, 名前・バイト数が全部合うのを確かめた
KIOXIA: /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 に置いた (35 ファイル 6335848626 バイト. Pod 側と名前・バイト数が全部合った)
ログを持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log22/train
Pod (id uirn5j6iy4xbyx) は消した (API で確かめた)
借りていた時間 188 分 2 秒 (3.13 時間) × 1時間 0.59 ドル = 約 1.85 ドル (約 292 円. 1ドル=158円)
由来: /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/train_provenance.json ・ /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/1499/train_provenance.json ・ Hugging Face (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3) にも置いた
ターミナルの記録: /Volumes/KIOXIA/r2r2r-data/lab/Log22/train/lab_train_20261106_195143.txt
```

### 学習の由来 train_provenance.json

#### lab/Log22/train/train_provenance.json

```json
{
  "schema_version": "provenance/1",
  "created_at": "2026-11-06T23:04:03+09:00",
  "producer": {
    "script": "lab_train.sh",
    "argv": [
      "--log",
      "Log22",
      "--model",
      "corr3"
    ],
    "machine": "shimadzueiichinoMacBook-Pro.local",
    "platform": "Darwin 25.6.0",
    "python": "3.12.12",
    "dry_run": false
  },
  "code": {
    "repo": "r2r2r-scripts",
    "commit": "284dfd731e64353c996486432811aebb5ee47ea0",
    "dirty": false,
    "dirty_files": [],
    "untracked_backups": [
      "pi05/build_demo_episodes.py.bak_pre_LOG352_20261019"
    ],
    "diff_sha256": null,
    "measured_at": "2026-11-06T19:51:43+09:00",
    "paths": [
      "lab_train.sh",
      "lab_lib.sh",
      "provenance.py",
      "pi05"
    ]
  },
  "external_code": [
    {
      "name": "openpi",
      "commit": "215abfb217dbac7d5f1273282331b9b1866c0479",
      "dirty": true,
      "dirty_files": [
        "src/openpi/training/config.py",
        "src/openpi/policies/mecharm_policy.py"
      ],
      "expected_commit": "215abfb"
    }
  ],
  "inputs": [
    {
      "kind": "dataset",
      "id": "mecharm_redblock_box_teach_v1_trim2_corr_v5",
      "path": "datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5",
      "fingerprint": "files-v2:e1e00dab2768e92af00099da4799de5dda1a3f2c171e029cd2558149d7d66171",
      "measured_at": "2026-11-06T19:51:44+09:00"
    },
    {
      "kind": "checkpoint",
      "id": "pi05_mecharm_trim_corr_lora_v2/1499",
      "path": "checkpoints/pi05_mecharm_trim_corr_lora_v2/1499",
      "fingerprint": "files-v2:2836021aa462c3099e5ca9f1b940ea70a11ac72a22572637d2df401e75c7a48a",
      "hf_repo": "PixelstoMotors/pi05-mecharm-trim-corr-lora-v2",
      "pod_path": "/workspace/ckpt/corr_v2_1499/params",
      "measured_at": "2026-11-06T19:51:44+09:00"
    },
    {
      "kind": "config",
      "id": "add_trim_config.py",
      "path": "lab/Log11/pod/add_trim_config.py",
      "fingerprint": "files-v2:3d7a5ce690b205880da44fa4b20de1b5905b7fb96a91580413ecb5cd660db44c",
      "sha256": "94059deb462623946344a5285f7ebd69c8041b5d820ed28fd4d383f7922f38a9",
      "measured_at": "2026-11-06T19:51:44+09:00"
    }
  ],
  "outputs": [
    {
      "kind": "checkpoint",
      "id": "pi05_mecharm_trim_corr_lora_v3/1499",
      "path": "checkpoints/pi05_mecharm_trim_corr_lora_v3/1499",
      "fingerprint": "files-v2:f980d30f8c936d1213e07304c84e0e1ae6bbcd974c9357728af86d0ac12fc0c1",
      "hf_repo": "PixelstoMotors/pi05-mecharm-trim-corr-lora-v3"
    }
  ],
  "config": {
    "name": "pi05_mecharm_trim_corr_v5_lora",
    "exp_name": "trim_corr_v5",
    "num_train_steps": 1500,
    "init_params": "/workspace/ckpt/corr_v2_1499/params",
    "data_name": "mecharm_redblock_box_teach_v1_trim2_corr_v5",
    "limit_hours": 5,
    "checkpoint_step": "1499"
  },
  "compute": {
    "provider": "runpod",
    "pod_id": "uirn5j6iy4xbyx",
    "gpu": "NVIDIA A40",
    "started_at": "2026-11-06T19:51:46+09:00",
    "train_started_at": "2026-11-06T19:57:02+09:00",
    "ended_at": "2026-11-06T22:59:48+09:00",
    "duration_sec": 11282,
    "cost_per_hr_usd": 0.59,
    "cost_usd": 1.85,
    "yen_per_usd": 158,
    "cost_line": "借りていた時間 188 分 2 秒 (3.13 時間) × 1時間 0.59 ドル = 約 1.85 ドル (約 292 円. 1ドル=158円)"
  },
  "numbering": {
    "series": "フィジカルAI研究室",
    "series_log": "Log22",
    "article_no": ""
  },
  "status": {
    "process": "completed",
    "task": "not_applicable"
  },
  "notes": {
    "hugging_face": "ok PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 に 35 ファイル 6,335,848,626 バイトを上げ, 名前・バイト数が全部合うのを確かめた",
    "kioxia": "/Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 に置いた (35 ファイル 6335848626 バイト. Pod 側と名前・バイト数が全部合った)"
  }
}
```

### 学習の Pod の記録 lab/Log22/train/lab_pod.json

#### lab/Log22/train/lab_pod.json

```json
{
  "id": "uirn5j6iy4xbyx",
  "kind": "train",
  "model": "corr3",
  "gpu": "NVIDIA A40",
  "log": "Log22",
  "costPerHr": 0.59,
  "created_epoch": 1793962306,
  "created_at": "2026-11-06 19:51:46",
  "config": "pi05_mecharm_trim_corr_v5_lora",
  "exp": "trim_corr_v5",
  "steps": 1500,
  "limit_sec": 18000,
  "limit_at": "2026-11-07 00:51:46",
  "init": "/workspace/ckpt/corr_v2_1499/params",
  "limit_hours": "5",
  "prov_start": {
    "code": {
      "repo": "r2r2r-scripts",
      "commit": "284dfd731e64353c996486432811aebb5ee47ea0",
      "dirty": false,
      "dirty_files": [],
      "untracked_backups": [
        "pi05/build_demo_episodes.py.bak_pre_LOG352_20261019"
      ],
      "diff_sha256": null,
      "measured_at": "2026-11-06T19:51:43+09:00",
      "paths": [
        "lab_train.sh",
        "lab_lib.sh",
        "provenance.py",
        "pi05"
      ]
    },
    "inputs": [
      {
        "kind": "dataset",
        "id": "mecharm_redblock_box_teach_v1_trim2_corr_v5",
        "path": "datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5",
        "fingerprint": "files-v2:e1e00dab2768e92af00099da4799de5dda1a3f2c171e029cd2558149d7d66171",
        "measured_at": "2026-11-06T19:51:44+09:00"
      },
      {
        "kind": "checkpoint",
        "id": "pi05_mecharm_trim_corr_lora_v2/1499",
        "path": "checkpoints/pi05_mecharm_trim_corr_lora_v2/1499",
        "fingerprint": "files-v2:2836021aa462c3099e5ca9f1b940ea70a11ac72a22572637d2df401e75c7a48a",
        "hf_repo": "PixelstoMotors/pi05-mecharm-trim-corr-lora-v2",
        "pod_path": "/workspace/ckpt/corr_v2_1499/params",
        "measured_at": "2026-11-06T19:51:44+09:00"
      },
      {
        "kind": "config",
        "id": "add_trim_config.py",
        "path": "lab/Log11/pod/add_trim_config.py",
        "fingerprint": "files-v2:3d7a5ce690b205880da44fa4b20de1b5905b7fb96a91580413ecb5cd660db44c",
        "sha256": "94059deb462623946344a5285f7ebd69c8041b5d820ed28fd4d383f7922f38a9",
        "measured_at": "2026-11-06T19:51:44+09:00"
      }
    ]
  },
  "watchdog_epoch": 1793983906,
  "watchdog": "WATCHDOG run=1 pod=1 key=1 ctl=1",
  "openpi": [
    {
      "name": "openpi",
      "commit": "215abfb217dbac7d5f1273282331b9b1866c0479",
      "dirty": true,
      "dirty_files": [
        "src/openpi/training/config.py",
        "src/openpi/policies/mecharm_policy.py"
      ],
      "expected_commit": "215abfb"
    }
  ],
  "train_started_epoch": 1793962622,
  "train_started_at": "2026-11-06 19:57:02",
  "terminated_epoch": 1793973588,
  "terminated_at": "2026-11-06 22:59:48",
  "rented_sec": 11282,
  "gone_confirmed": true
}
```

### Pod の準備の記録 prep_trim_corr.log

#### lab/Log22/train/prep_trim_corr.log

```text
== 1. tar を広げる → /workspace/data/mecharm_redblock_box_teach_v1_trim2_corr_v5
   エピソード 30 本 (Mac で作ったときは 30 本)
== 2. 学習設定 pi05_mecharm_trim_corr_v5_lora を足す (steps=1500 init=/workspace/ckpt/corr_v2_1499/params)
installed: pi05_mecharm_trim_corr_v5_lora (repo_id=local/mecharm_redblock_box_teach_v1_trim2_corr_v5, 1500 steps, init=/workspace/ckpt/corr_v2_1499/params)
== 3. LeRobot 形式に変換 (local/mecharm_redblock_box_teach_v1_trim2_corr_v5)

Map:   0%|          | 0/118 [00:00<?, ? examples/s]
Map:  84%|████████▍ | 99/118 [00:00<00:00, 972.36 examples/s]
Map: 100%|██████████| 118/118 [00:00<00:00, 846.40 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 90.30ba/s]
  pi05_20261031_175527: 118 steps

Map:   0%|          | 0/152 [00:00<?, ? examples/s]
Map:  85%|████████▍ | 129/152 [00:00<00:00, 1271.54 examples/s]
Map: 100%|██████████| 152/152 [00:00<00:00, 1084.97 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 89.94ba/s]
  pi05_20261031_180435: 152 steps

Map:   0%|          | 0/126 [00:00<?, ? examples/s]
Map: 100%|██████████| 126/126 [00:00<00:00, 1098.62 examples/s]
Map: 100%|██████████| 126/126 [00:00<00:00, 1070.64 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 83.71ba/s]
  pi05_20261031_181946: 126 steps

Map:   0%|          | 0/133 [00:00<?, ? examples/s]
Map: 100%|██████████| 133/133 [00:00<00:00, 1219.33 examples/s]
Map: 100%|██████████| 133/133 [00:00<00:00, 1191.78 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 114.57ba/s]
  pi05_20261031_184016: 133 steps

Map:   0%|          | 0/115 [00:00<?, ? examples/s]
Map: 100%|██████████| 115/115 [00:00<00:00, 1259.43 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 88.77ba/s]
  pi05_20261101_190347: 115 steps

Map:   0%|          | 0/145 [00:00<?, ? examples/s]
Map:  90%|████████▉ | 130/145 [00:00<00:00, 1280.71 examples/s]
Map: 100%|██████████| 145/145 [00:00<00:00, 1096.39 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 77.33ba/s]
  pi05_20261101_190854: 145 steps

Map:   0%|          | 0/104 [00:00<?, ? examples/s]
Map: 100%|██████████| 104/104 [00:00<00:00, 1184.46 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 100.10ba/s]
  pi05_20261101_192101: 104 steps

Map:   0%|          | 0/188 [00:00<?, ? examples/s]
Map:  49%|████▉     | 92/188 [00:00<00:00, 898.09 examples/s]
Map: 100%|██████████| 188/188 [00:00<00:00, 933.81 examples/s]
Map: 100%|██████████| 188/188 [00:00<00:00, 915.68 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 76.15ba/s]
  pi05_20261106_183926: 188 steps

Map:   0%|          | 0/190 [00:00<?, ? examples/s]
Map:  65%|██████▌   | 124/190 [00:00<00:00, 1223.93 examples/s]
Map: 100%|██████████| 190/190 [00:00<00:00, 1116.45 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 82.95ba/s]
  pi05_20261106_184452: 190 steps

Map:   0%|          | 0/178 [00:00<?, ? examples/s]
Map:  77%|███████▋  | 137/178 [00:00<00:00, 1355.49 examples/s]
Map: 100%|██████████| 178/178 [00:00<00:00, 1201.71 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 105.06ba/s]
  pi05_20261106_184650: 178 steps

Map:   0%|          | 0/135 [00:00<?, ? examples/s]
Map: 100%|██████████| 135/135 [00:00<00:00, 1279.14 examples/s]
Map: 100%|██████████| 135/135 [00:00<00:00, 1249.93 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 92.10ba/s]
  pi05_20261106_184906: 135 steps

Map:   0%|          | 0/144 [00:00<?, ? examples/s]
Map:  98%|█████████▊| 141/144 [00:00<00:00, 1397.81 examples/s]
Map: 100%|██████████| 144/144 [00:00<00:00, 1260.94 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 88.36ba/s]
  run_0001_20261019_183454: 144 steps

Map:   0%|          | 0/147 [00:00<?, ? examples/s]
Map:  67%|██████▋   | 98/147 [00:00<00:00, 968.23 examples/s]
Map: 100%|██████████| 147/147 [00:00<00:00, 863.83 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 55.79ba/s]
  run_0001_20261019_191257: 147 steps

Map:   0%|          | 0/144 [00:00<?, ? examples/s]
Map:  95%|█████████▌| 137/144 [00:00<00:00, 1355.84 examples/s]
Map: 100%|██████████| 144/144 [00:00<00:00, 1175.39 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 79.65ba/s]
  run_0001_20261019_191347: 144 steps

Map:   0%|          | 0/163 [00:00<?, ? examples/s]
Map:  72%|███████▏  | 118/163 [00:00<00:00, 1163.69 examples/s]
Map: 100%|██████████| 163/163 [00:00<00:00, 956.02 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 61.18ba/s]
  run_0001_20261019_191441: 163 steps

Map:   0%|          | 0/139 [00:00<?, ? examples/s]
Map:  73%|███████▎  | 102/139 [00:00<00:00, 1005.16 examples/s]
Map: 100%|██████████| 139/139 [00:00<00:00, 864.78 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 73.55ba/s]
  run_0001_20261019_191541: 139 steps

Map:   0%|          | 0/138 [00:00<?, ? examples/s]
Map:  76%|███████▌  | 105/138 [00:00<00:00, 1039.93 examples/s]
Map: 100%|██████████| 138/138 [00:00<00:00, 972.58 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 84.37ba/s]
  run_0001_20261019_191635: 138 steps

Map:   0%|          | 0/140 [00:00<?, ? examples/s]
Map:  97%|█████████▋| 136/140 [00:00<00:00, 1347.88 examples/s]
Map: 100%|██████████| 140/140 [00:00<00:00, 1168.10 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 96.59ba/s]
  run_0001_20261019_192343: 140 steps

Map:   0%|          | 0/138 [00:00<?, ? examples/s]
Map:  95%|█████████▍| 131/138 [00:00<00:00, 1302.34 examples/s]
Map: 100%|██████████| 138/138 [00:00<00:00, 1118.87 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 114.55ba/s]
  run_0001_20261019_192438: 138 steps

Map:   0%|          | 0/140 [00:00<?, ? examples/s]
Map:  99%|█████████▊| 138/140 [00:00<00:00, 1361.52 examples/s]
Map: 100%|██████████| 140/140 [00:00<00:00, 1194.44 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 76.66ba/s]
  run_0001_20261019_192539: 140 steps

Map:   0%|          | 0/137 [00:00<?, ? examples/s]
Map:  85%|████████▌ | 117/137 [00:00<00:00, 1149.13 examples/s]
Map: 100%|██████████| 137/137 [00:00<00:00, 1042.42 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 78.92ba/s]
  run_0001_20261019_192633: 137 steps

Map:   0%|          | 0/164 [00:00<?, ? examples/s]
Map:  58%|█████▊    | 95/164 [00:00<00:00, 926.76 examples/s]
Map: 100%|██████████| 164/164 [00:00<00:00, 803.84 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 50.97ba/s]
  run_0001_20261019_193404: 164 steps

Map:   0%|          | 0/159 [00:00<?, ? examples/s]
Map:  67%|██████▋   | 107/159 [00:00<00:00, 1055.78 examples/s]
Map: 100%|██████████| 159/159 [00:00<00:00, 945.11 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 57.83ba/s]
  run_0001_20261019_193459: 159 steps

Map:   0%|          | 0/156 [00:00<?, ? examples/s]
Map:  76%|███████▌  | 118/156 [00:00<00:00, 1163.21 examples/s]
Map: 100%|██████████| 156/156 [00:00<00:00, 1098.09 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 117.17ba/s]
  run_0001_20261019_193617: 156 steps

Map:   0%|          | 0/165 [00:00<?, ? examples/s]
Map:  79%|███████▉  | 130/165 [00:00<00:00, 1279.45 examples/s]
Map: 100%|██████████| 165/165 [00:00<00:00, 1118.68 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 91.08ba/s]
  run_0001_20261019_193825: 165 steps

Map:   0%|          | 0/151 [00:00<?, ? examples/s]
Map:  89%|████████▉ | 135/151 [00:00<00:00, 1339.38 examples/s]
Map: 100%|██████████| 151/151 [00:00<00:00, 1141.87 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 57.94ba/s]
  run_0001_20261019_193932: 151 steps

Map:   0%|          | 0/123 [00:00<?, ? examples/s]
Map: 100%|██████████| 123/123 [00:00<00:00, 1060.43 examples/s]
Map: 100%|██████████| 123/123 [00:00<00:00, 1034.37 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 77.36ba/s]
  run_0001_20261019_194818: 123 steps

Map:   0%|          | 0/126 [00:00<?, ? examples/s]
Map:  92%|█████████▏| 116/126 [00:00<00:00, 1150.81 examples/s]
Map: 100%|██████████| 126/126 [00:00<00:00, 1008.68 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 74.45ba/s]
  run_0001_20261019_194913: 126 steps

Map:   0%|          | 0/123 [00:00<?, ? examples/s]
Map:  97%|█████████▋| 119/123 [00:00<00:00, 1174.84 examples/s]
Map: 100%|██████████| 123/123 [00:00<00:00, 999.16 examples/s] 

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 83.82ba/s]
  run_0001_20261019_195023: 123 steps

Map:   0%|          | 0/120 [00:00<?, ? examples/s]
Map: 100%|██████████| 120/120 [00:00<00:00, 1110.76 examples/s]
Map: 100%|██████████| 120/120 [00:00<00:00, 1085.65 examples/s]

Creating parquet from Arrow format:   0%|          | 0/2 [00:00<?, ?ba/s]
Creating parquet from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 94.67ba/s]
  run_0001_20261019_195120: 120 steps
done: 30 episodes / 4301 frames → /root/.cache/huggingface/lerobot/local/mecharm_redblock_box_teach_v1_trim2_corr_v5
== 4. 正規化の値は trim2 の値をそのまま置く (計算し直さない)
   /workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json → /workspace/openpi/assets/pi05_mecharm_trim_corr_v5_lora/local/mecharm_redblock_box_teach_v1_trim2_corr_v5/norm_stats.json (sha256 80346837eb12…)
== 5. 準備できました. 学習はこのコマンドで始める (このスクリプトでは流さない):
   cd /workspace/openpi && XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 uv run scripts/train.py pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5 --overwrite 2>&1 | tee /workspace/logs/train_trim_corr_v5.log
```

### 学習の記録 train_trim_corr_v5.log

#### lab/Log22/train/train_trim_corr_v5.log

```text
10:57:13.096 [I] Running on: a4e3c12d84ee                                                         (1084:train.py:196)
INFO:2026-11-06 10:57:13,626:jax._src.xla_bridge:925: Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
10:57:13.626 [I] Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig' (1084:xla_bridge.py:925)
INFO:2026-11-06 10:57:13,631:jax._src.xla_bridge:925: Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
10:57:13.631 [I] Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory (1084:xla_bridge.py:925)
10:57:14.790 [I] Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x7664e0596590> (1084:base_pytree_checkpoint_handler.py:334)
10:57:14.790 [I] Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x7664e0596590> (1084:base_pytree_checkpoint_handler.py:334)
10:57:14.790 [I] [thread=MainThread] Failed to get flag value for EXPERIMENTAL_ORBAX_USE_DISTRIBUTED_PROCESS_ID. (1084:multihost.py:390)
10:57:14.791 [I] [process=0][thread=MainThread] CheckpointManager init: checkpointers=None, item_names=None, item_handlers={'assets': <openpi.training.checkpoints.CallbackHandler object at 0x7663418e5910>, 'train_state': <orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x7663418e58d0>, 'params': <orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x766342f15d50>}, handler_registry=None (1084:checkpoint_manager.py:620)
10:57:14.791 [I] Deferred registration for item: "assets". Adding handler `<openpi.training.checkpoints.CallbackHandler object at 0x7663418e5910>` for item "assets" and save args `<class 'openpi.training.checkpoints.CallbackSave'>` and restore args `<class 'openpi.training.checkpoints.CallbackRestore'>` to `_handler_registry`. (1084:composite_checkpoint_handler.py:234)
10:57:14.791 [I] Deferred registration for item: "train_state". Adding handler `<orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x7663418e58d0>` for item "train_state" and save args `<class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeSaveArgs'>` and restore args `<class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeRestoreArgs'>` to `_handler_registry`. (1084:composite_checkpoint_handler.py:234)
10:57:14.791 [I] Deferred registration for item: "params". Adding handler `<orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x766342f15d50>` for item "params" and save args `<class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeSaveArgs'>` and restore args `<class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeRestoreArgs'>` to `_handler_registry`. (1084:composite_checkpoint_handler.py:234)
10:57:14.791 [I] Deferred registration for item: "metrics". Adding handler `<orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonCheckpointHandler object at 0x766342fe0910>` for item "metrics" and save args `<class 'orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonSaveArgs'>` and restore args `<class 'orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonRestoreArgs'>` to `_handler_registry`. (1084:composite_checkpoint_handler.py:234)
10:57:14.791 [I] Initialized registry DefaultCheckpointHandlerRegistry({('assets', <class 'openpi.training.checkpoints.CallbackSave'>): <openpi.training.checkpoints.CallbackHandler object at 0x7663418e5910>, ('assets', <class 'openpi.training.checkpoints.CallbackRestore'>): <openpi.training.checkpoints.CallbackHandler object at 0x7663418e5910>, ('train_state', <class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeSaveArgs'>): <orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x7663418e58d0>, ('train_state', <class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeRestoreArgs'>): <orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x7663418e58d0>, ('params', <class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeSaveArgs'>): <orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x766342f15d50>, ('params', <class 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeRestoreArgs'>): <orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler object at 0x766342f15d50>, ('metrics', <class 'orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonSaveArgs'>): <orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonCheckpointHandler object at 0x766342fe0910>, ('metrics', <class 'orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonRestoreArgs'>): <orbax.checkpoint._src.handlers.json_checkpoint_handler.JsonCheckpointHandler object at 0x766342fe0910>}). (1084:composite_checkpoint_handler.py:502)
10:57:14.791 [I] orbax-checkpoint version: 0.11.13                                                (1084:abstract_checkpointer.py:35)
10:57:14.792 [I] [process=0][thread=MainThread] Using barrier_sync_fn: <function get_barrier_sync_fn.<locals>.<lambda> at 0x766341966840> timeout: 7200 secs and primary_host=0 for async checkpoint writes (1084:async_checkpointer.py:170)
10:57:14.792 [I] Found 0 checkpoint steps in /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5 (1084:checkpoint_manager.py:1701)
10:57:14.792 [I] [process=0][thread=MainThread] CheckpointManager created,  primary_host=0, CheckpointManagerOptions=CheckpointManagerOptions(save_interval_steps=1, max_to_keep=1, keep_time_interval=None, keep_period=1000, should_keep_fn=None, best_fn=None, best_mode='max', keep_checkpoints_without_metrics=True, step_prefix=None, step_format_fixed_length=None, step_name_format=None, create=False, cleanup_tmp_directories=False, save_on_steps=frozenset(), single_host_load_and_broadcast=False, todelete_subdir=None, enable_background_delete=False, read_only=False, enable_async_checkpointing=True, async_options=AsyncOptions(timeout_secs=7200, barrier_sync_fn=None, post_finalization_callback=None, create_directories_asynchronously=True), multiprocessing_options=MultiprocessingOptions(primary_host=0, active_processes=None, barrier_sync_key_prefix=None), should_save_fn=None, file_options=FileOptions(path_permission_mode=None), save_root_metadata=True, temporary_path_class=None, save_decision_policy=None, prevent_write_metrics=False), root_directory=/workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5: <orbax.checkpoint.checkpoint_manager.CheckpointManager object at 0x766341be4c10> (1084:checkpoint_manager.py:801)
10:57:14.807 [I] Downloading gs://big_vision/paligemma_tokenizer.model to /root/.cache/openpi/big_vision/paligemma_tokenizer.model (1084:download.py:86)
10:57:15.297 [I] Progress on: -/4264023 rate:- remaining:? elapsed:00:00 postfix:-                (1084:tqdm_logging.py:145)
10:57:16.299 [I] Progress on: 4.07MiB/4.07MiB rate:4,258,692.4iB/s remaining:00:00 elapsed:00:01 postfix:- (1084:tqdm_logging.py:145)
10:57:16.460 [I] Loaded norm stats from /workspace/openpi/assets/pi05_mecharm_trim_corr_v5_lora/local/mecharm_redblock_box_teach_v1_trim2_corr_v5 (1084:config.py:197)
10:57:16.462 [I] data_config: DataConfig(repo_id='local/mecharm_redblock_box_teach_v1_trim2_corr_v5', asset_id='local/mecharm_redblock_box_teach_v1_trim2_corr_v5', norm_stats={'state': NormStats(mean=array([  0.11632019,  47.20463562,   4.25571823,   3.47346711,
       -49.80432129, -32.3266716 ,   0.42522314]), std=array([33.52104568, 21.01031494, 13.69488239,  5.18986511, 21.06582069,
        3.66200471,  0.49437681]), q01=array([-61.62614893,   1.48315994, -34.35241028,  -7.64541887,
       -86.05951349, -40.59999847,   0.        ]), q99=array([ 44.61974199,  87.87268339,  30.92747083,  16.58067341,
        -0.260924  , -26.45196724,   0.9998    ])), 'actions': NormStats(mean=array([ 0.192315  ,  0.027426  , -0.01522407,  0.04785711, -0.08074823,
       -0.05333935,  0.42522326]), std=array([9.28794479, 9.00317574, 4.14188147, 1.60169351, 9.50789356,
       0.9818573 , 0.49437684]), q01=array([-21.17317298, -23.18362675, -14.27432952,  -5.18250646,
       -26.67564539,  -3.16441521,   0.        ]), q99=array([27.84317563, 22.359031  , 11.17634333,  4.02140303, 25.71650509,
        3.24633011,  0.9998    ]))}, repack_transforms=Group(inputs=[RepackTransform(structure={'observation/image': 'image', 'observation/state': 'state', 'actions': 'actions', 'prompt': 'prompt'})], outputs=()), data_transforms=Group(inputs=(MecharmInputs(model_type=<ModelType.PI05: 'pi05'>), DeltaActions(mask=(True, True, True, True, True, True, False))), outputs=(AbsoluteActions(mask=(True, True, True, True, True, True, False)), MecharmOutputs())), model_transforms=Group(inputs=[InjectDefaultPrompt(prompt=None), ResizeImages(height=224, width=224), TokenizePrompt(tokenizer=<openpi.models.tokenizer.PaligemmaTokenizer object at 0x7663426d5950>, discrete_state_input=True), PadStatesAndActions(model_action_dim=32)], outputs=()), use_quantile_norm=True, action_sequence_keys=('actions',), prompt_from_task=True, rlds_data_dir=None, action_space=None, datasets=()) (1084:data_loader.py:243)

Downloading data:   0%|          | 0/30 [00:00<?, ?files/s]
Downloading data: 100%|██████████| 30/30 [00:00<00:00, 63389.98files/s]

Generating train split: 0 examples [00:00, ? examples/s]
Generating train split: 496 examples [00:00, 2611.14 examples/s]
Generating train split: 993 examples [00:00, 2406.98 examples/s]
Generating train split: 1371 examples [00:00, 2543.03 examples/s]
Generating train split: 1828 examples [00:00, 2972.41 examples/s]
Generating train split: 2321 examples [00:00, 3385.30 examples/s]
Generating train split: 2837 examples [00:00, 3741.62 examples/s]
Generating train split: 3437 examples [00:01, 4147.41 examples/s]
Generating train split: 4032 examples [00:01, 4238.63 examples/s]
Generating train split: 4301 examples [00:01, 3608.88 examples/s]
10:57:18.011 [I] local_batch_size: 32                                                             (1084:data_loader.py:324)
10:57:34.787 [I] Initialized data loader:
[0].images['base_0_rgb']: (32, 224, 224, 3)@float32
[0].images['left_wrist_0_rgb']: (32, 224, 224, 3)@float32
[0].images['right_wrist_0_rgb']: (32, 224, 224, 3)@float32
[0].image_masks['base_0_rgb']: (32,)@bool
[0].image_masks['left_wrist_0_rgb']: (32,)@bool
[0].image_masks['right_wrist_0_rgb']: (32,)@bool
[0].state: (32, 32)@float32
[0].tokenized_prompt: (32, 200)@int32
[0].tokenized_prompt_mask: (32, 200)@bool
[1]: (32, 10, 32)@float32 (1084:train.py:227)
10:57:36.304 [I] Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x7664e0596590> (1084:base_pytree_checkpoint_handler.py:334)
10:57:36.336 [I] Restoring checkpoint from /workspace/ckpt/corr_v2_1499/params.                   (1084:checkpointer.py:298)
10:57:41.428 [I] [process=0] /jax/checkpoint/read/bytes_per_sec: 1.4 GiB/s (total bytes: 7.2 GiB) (time elapsed: 5 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
10:57:41.429 [I] Finished restoring checkpoint in 5.09 seconds from /workspace/ckpt/corr_v2_1499/params. (1084:checkpointer.py:309)
10:57:44.800 [I] Initialized train state:
['PaliGemma']['img']['Transformer']['encoder_norm']['bias'].value: (1152,)@float32
['PaliGemma']['img']['Transformer']['encoder_norm']['scale'].value: (1152,)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['LayerNorm_0']['bias'].value: (27, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['LayerNorm_0']['scale'].value: (27, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['LayerNorm_1']['bias'].value: (27, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['LayerNorm_1']['scale'].value: (27, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MlpBlock_0']['Dense_0']['bias'].value: (27, 4304)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MlpBlock_0']['Dense_0']['kernel'].value: (27, 1152, 4304)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MlpBlock_0']['Dense_1']['bias'].value: (27, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MlpBlock_0']['Dense_1']['kernel'].value: (27, 4304, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['key']['bias'].value: (27, 16, 72)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['key']['kernel'].value: (27, 1152, 16, 72)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['out']['bias'].value: (27, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['out']['kernel'].value: (27, 16, 72, 1152)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['query']['bias'].value: (27, 16, 72)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['query']['kernel'].value: (27, 1152, 16, 72)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['value']['bias'].value: (27, 16, 72)@float32
['PaliGemma']['img']['Transformer']['encoderblock']['MultiHeadDotProductAttention_0']['value']['kernel'].value: (27, 1152, 16, 72)@float32
['PaliGemma']['img']['embedding']['bias'].value: (1152,)@float32
['PaliGemma']['img']['embedding']['kernel'].value: (14, 14, 3, 1152)@float32
['PaliGemma']['img']['head']['bias'].value: (2048,)@float32
['PaliGemma']['img']['head']['kernel'].value: (1152, 2048)@float32
['PaliGemma']['img']['pos_embedding'].value: (1, 256, 1152)@float32
['PaliGemma']['llm']['embedder']['input_embedding'].value: (257152, 2048)@bfloat16
['PaliGemma']['llm']['final_norm']['scale'].value: (2048,)@bfloat16
['PaliGemma']['llm']['final_norm_1']['Dense_0']['bias'].value: (3072,)@bfloat16
['PaliGemma']['llm']['final_norm_1']['Dense_0']['kernel'].value: (1024, 3072)@bfloat16
['PaliGemma']['llm']['layers']['attn']['attn_vec_einsum']['lora_a'].value: (18, 8, 256, 16)@float32
['PaliGemma']['llm']['layers']['attn']['attn_vec_einsum']['lora_b'].value: (18, 8, 16, 2048)@float32
['PaliGemma']['llm']['layers']['attn']['attn_vec_einsum']['w'].value: (18, 8, 256, 2048)@bfloat16
['PaliGemma']['llm']['layers']['attn']['attn_vec_einsum_1']['lora_a'].value: (18, 8, 256, 32)@float32
['PaliGemma']['llm']['layers']['attn']['attn_vec_einsum_1']['lora_b'].value: (18, 8, 32, 1024)@float32
['PaliGemma']['llm']['layers']['attn']['attn_vec_einsum_1']['w'].value: (18, 8, 256, 1024)@bfloat16
['PaliGemma']['llm']['layers']['attn']['kv_einsum']['lora_a'].value: (18, 2, 1, 2048, 16)@float32
['PaliGemma']['llm']['layers']['attn']['kv_einsum']['lora_b'].value: (18, 2, 1, 16, 256)@float32
['PaliGemma']['llm']['layers']['attn']['kv_einsum']['w'].value: (18, 2, 1, 2048, 256)@bfloat16
['PaliGemma']['llm']['layers']['attn']['kv_einsum_1']['lora_a'].value: (18, 2, 1, 1024, 32)@float32
['PaliGemma']['llm']['layers']['attn']['kv_einsum_1']['lora_b'].value: (18, 2, 1, 32, 256)@float32
['PaliGemma']['llm']['layers']['attn']['kv_einsum_1']['w'].value: (18, 2, 1, 1024, 256)@bfloat16
['PaliGemma']['llm']['layers']['attn']['q_einsum']['lora_a'].value: (18, 8, 2048, 16)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum']['lora_b'].value: (18, 8, 16, 256)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum']['w'].value: (18, 8, 2048, 256)@bfloat16
['PaliGemma']['llm']['layers']['attn']['q_einsum_1']['lora_a'].value: (18, 8, 1024, 32)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum_1']['lora_b'].value: (18, 8, 32, 256)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum_1']['w'].value: (18, 8, 1024, 256)@bfloat16
['PaliGemma']['llm']['layers']['mlp']['gating_einsum'].value: (18, 2, 2048, 16384)@bfloat16
['PaliGemma']['llm']['layers']['mlp']['gating_einsum_lora_a'].value: (18, 2, 2048, 16)@float32
['PaliGemma']['llm']['layers']['mlp']['gating_einsum_lora_b'].value: (18, 2, 16, 16384)@float32
['PaliGemma']['llm']['layers']['mlp']['linear'].value: (18, 16384, 2048)@bfloat16
['PaliGemma']['llm']['layers']['mlp']['linear_lora_a'].value: (18, 16384, 16)@float32
['PaliGemma']['llm']['layers']['mlp']['linear_lora_b'].value: (18, 16, 2048)@float32
['PaliGemma']['llm']['layers']['mlp_1']['gating_einsum'].value: (18, 2, 1024, 4096)@bfloat16
['PaliGemma']['llm']['layers']['mlp_1']['gating_einsum_lora_a'].value: (18, 2, 1024, 32)@float32
['PaliGemma']['llm']['layers']['mlp_1']['gating_einsum_lora_b'].value: (18, 2, 32, 4096)@float32
['PaliGemma']['llm']['layers']['mlp_1']['linear'].value: (18, 4096, 1024)@bfloat16
['PaliGemma']['llm']['layers']['mlp_1']['linear_lora_a'].value: (18, 4096, 32)@float32
['PaliGemma']['llm']['layers']['mlp_1']['linear_lora_b'].value: (18, 32, 1024)@float32
['PaliGemma']['llm']['layers']['pre_attention_norm']['scale'].value: (18, 2048)@bfloat16
['PaliGemma']['llm']['layers']['pre_attention_norm_1']['Dense_0']['bias'].value: (18, 3072)@bfloat16
['PaliGemma']['llm']['layers']['pre_attention_norm_1']['Dense_0']['kernel'].value: (18, 1024, 3072)@bfloat16
['PaliGemma']['llm']['layers']['pre_ffw_norm']['scale'].value: (18, 2048)@bfloat16
['PaliGemma']['llm']['layers']['pre_ffw_norm_1']['Dense_0']['bias'].value: (18, 3072)@bfloat16
['PaliGemma']['llm']['layers']['pre_ffw_norm_1']['Dense_0']['kernel'].value: (18, 1024, 3072)@bfloat16
['action_in_proj']['bias'].value: (1024,)@float32
['action_in_proj']['kernel'].value: (32, 1024)@float32
['action_out_proj']['bias'].value: (32,)@float32
['action_out_proj']['kernel'].value: (1024, 32)@float32
['time_mlp_in']['bias'].value: (1024,)@float32
['time_mlp_in']['kernel'].value: (1024, 1024)@float32
['time_mlp_out']['bias'].value: (1024,)@float32
['time_mlp_out']['kernel'].value: (1024, 1024)@float32 (1084:train.py:238)
10:57:44.810 [I] Progress on: -/1500 rate:- remaining:? elapsed:00:00 postfix:-                   (1084:tqdm_logging.py:145)


Step 0: grad_norm=0.0816, loss=0.0129, param_norm=1804.2159
11:02:03.514 [I] Progress on: -/1500 rate:- remaining:? elapsed:04:18 postfix:-                   (1084:tqdm_logging.py:145)
11:02:17.889 [I] Progress on: 4.00it/1.50kit rate:39.8s/it remaining:16:32:14 elapsed:04:33 postfix:- (1084:tqdm_logging.py:145)
11:02:31.893 [I] Progress on: 6.00it/1.50kit rate:20.8s/it remaining:8:38:57 elapsed:04:47 postfix:- (1084:tqdm_logging.py:145)
11:02:45.882 [I] Progress on: 8.00it/1.50kit rate:13.3s/it remaining:5:31:55 elapsed:05:01 postfix:- (1084:tqdm_logging.py:145)
11:02:59.918 [I] Progress on: 10.0it/1.50kit rate:10.0s/it remaining:4:09:00 elapsed:05:15 postfix:- (1084:tqdm_logging.py:145)
11:03:13.986 [I] Progress on: 12.0it/1.50kit rate:8.5s/it remaining:3:30:17 elapsed:05:29 postfix:- (1084:tqdm_logging.py:145)
11:03:28.073 [I] Progress on: 14.0it/1.50kit rate:7.7s/it remaining:3:11:46 elapsed:05:43 postfix:- (1084:tqdm_logging.py:145)
11:03:42.158 [I] Progress on: 16.0it/1.50kit rate:7.4s/it remaining:3:02:41 elapsed:05:57 postfix:- (1084:tqdm_logging.py:145)
11:03:56.265 [I] Progress on: 18.0it/1.50kit rate:7.2s/it remaining:2:58:14 elapsed:06:11 postfix:- (1084:tqdm_logging.py:145)
11:04:10.379 [I] Progress on: 20.0it/1.50kit rate:7.1s/it remaining:2:55:59 elapsed:06:25 postfix:- (1084:tqdm_logging.py:145)
11:04:24.487 [I] Progress on: 22.0it/1.50kit rate:7.1s/it remaining:2:54:44 elapsed:06:39 postfix:- (1084:tqdm_logging.py:145)
11:04:38.587 [I] Progress on: 24.0it/1.50kit rate:7.1s/it remaining:2:53:57 elapsed:06:53 postfix:- (1084:tqdm_logging.py:145)
11:04:52.739 [I] Progress on: 26.0it/1.50kit rate:7.1s/it remaining:2:53:47 elapsed:07:07 postfix:- (1084:tqdm_logging.py:145)
11:05:06.872 [I] Progress on: 28.0it/1.50kit rate:7.1s/it remaining:2:53:26 elapsed:07:22 postfix:- (1084:tqdm_logging.py:145)
11:05:21.005 [I] Progress on: 30.0it/1.50kit rate:7.1s/it remaining:2:53:09 elapsed:07:36 postfix:- (1084:tqdm_logging.py:145)
11:05:35.157 [I] Progress on: 32.0it/1.50kit rate:7.1s/it remaining:2:53:01 elapsed:07:50 postfix:- (1084:tqdm_logging.py:145)
11:05:49.273 [I] Progress on: 34.0it/1.50kit rate:7.1s/it remaining:2:52:37 elapsed:08:04 postfix:- (1084:tqdm_logging.py:145)
11:06:03.396 [I] Progress on: 36.0it/1.50kit rate:7.1s/it remaining:2:52:19 elapsed:08:18 postfix:- (1084:tqdm_logging.py:145)
11:06:17.542 [I] Progress on: 38.0it/1.50kit rate:7.1s/it remaining:2:52:10 elapsed:08:32 postfix:- (1084:tqdm_logging.py:145)
11:06:31.671 [I] Progress on: 40.0it/1.50kit rate:7.1s/it remaining:2:51:55 elapsed:08:46 postfix:- (1084:tqdm_logging.py:145)
11:06:45.812 [I] Progress on: 42.0it/1.50kit rate:7.1s/it remaining:2:51:44 elapsed:09:01 postfix:- (1084:tqdm_logging.py:145)
11:06:59.964 [I] Progress on: 44.0it/1.50kit rate:7.1s/it remaining:2:51:37 elapsed:09:15 postfix:- (1084:tqdm_logging.py:145)
11:07:14.094 [I] Progress on: 46.0it/1.50kit rate:7.1s/it remaining:2:51:16 elapsed:09:29 postfix:- (1084:tqdm_logging.py:145)
11:07:28.232 [I] Progress on: 48.0it/1.50kit rate:7.1s/it remaining:2:51:03 elapsed:09:43 postfix:- (1084:tqdm_logging.py:145)
11:07:42.371 [I] Progress on: 50.0it/1.50kit rate:7.1s/it remaining:2:50:49 elapsed:09:57 postfix:- (1084:tqdm_logging.py:145)


Step 50: grad_norm=0.0707, loss=0.0066, param_norm=1804.2159
11:07:56.270 [I] Progress on: 50.0it/1.50kit rate:7.1s/it remaining:2:50:49 elapsed:10:11 postfix:- (1084:tqdm_logging.py:145)
11:08:10.804 [I] Progress on: 54.0it/1.50kit rate:6.8s/it remaining:2:43:26 elapsed:10:25 postfix:- (1084:tqdm_logging.py:145)
11:08:24.902 [I] Progress on: 56.0it/1.50kit rate:6.9s/it remaining:2:46:28 elapsed:10:40 postfix:- (1084:tqdm_logging.py:145)
11:08:39.013 [I] Progress on: 58.0it/1.50kit rate:7.0s/it remaining:2:47:56 elapsed:10:54 postfix:- (1084:tqdm_logging.py:145)
11:08:53.106 [I] Progress on: 60.0it/1.50kit rate:7.0s/it remaining:2:48:25 elapsed:11:08 postfix:- (1084:tqdm_logging.py:145)
11:09:07.212 [I] Progress on: 62.0it/1.50kit rate:7.0s/it remaining:2:48:34 elapsed:11:22 postfix:- (1084:tqdm_logging.py:145)
11:09:21.325 [I] Progress on: 64.0it/1.50kit rate:7.0s/it remaining:2:48:34 elapsed:11:36 postfix:- (1084:tqdm_logging.py:145)
11:09:35.416 [I] Progress on: 66.0it/1.50kit rate:7.0s/it remaining:2:48:18 elapsed:11:50 postfix:- (1084:tqdm_logging.py:145)
11:09:49.510 [I] Progress on: 68.0it/1.50kit rate:7.0s/it remaining:2:48:09 elapsed:12:04 postfix:- (1084:tqdm_logging.py:145)
11:10:03.599 [I] Progress on: 70.0it/1.50kit rate:7.0s/it remaining:2:47:56 elapsed:12:18 postfix:- (1084:tqdm_logging.py:145)
11:10:17.701 [I] Progress on: 72.0it/1.50kit rate:7.1s/it remaining:2:47:47 elapsed:12:32 postfix:- (1084:tqdm_logging.py:145)
11:10:31.802 [I] Progress on: 74.0it/1.50kit rate:7.1s/it remaining:2:47:35 elapsed:12:46 postfix:- (1084:tqdm_logging.py:145)
11:10:45.925 [I] Progress on: 76.0it/1.50kit rate:7.1s/it remaining:2:47:27 elapsed:13:01 postfix:- (1084:tqdm_logging.py:145)
11:11:00.039 [I] Progress on: 78.0it/1.50kit rate:7.1s/it remaining:2:47:12 elapsed:13:15 postfix:- (1084:tqdm_logging.py:145)
11:11:14.153 [I] Progress on: 80.0it/1.50kit rate:7.1s/it remaining:2:47:00 elapsed:13:29 postfix:- (1084:tqdm_logging.py:145)
11:11:28.259 [I] Progress on: 82.0it/1.50kit rate:7.1s/it remaining:2:46:42 elapsed:13:43 postfix:- (1084:tqdm_logging.py:145)
11:11:42.381 [I] Progress on: 84.0it/1.50kit rate:7.1s/it remaining:2:46:32 elapsed:13:57 postfix:- (1084:tqdm_logging.py:145)
11:11:56.511 [I] Progress on: 86.0it/1.50kit rate:7.1s/it remaining:2:46:26 elapsed:14:11 postfix:- (1084:tqdm_logging.py:145)
11:12:10.632 [I] Progress on: 88.0it/1.50kit rate:7.1s/it remaining:2:46:13 elapsed:14:25 postfix:- (1084:tqdm_logging.py:145)
11:12:24.734 [I] Progress on: 90.0it/1.50kit rate:7.1s/it remaining:2:45:52 elapsed:14:39 postfix:- (1084:tqdm_logging.py:145)
11:12:38.823 [I] Progress on: 92.0it/1.50kit rate:7.1s/it remaining:2:45:26 elapsed:14:54 postfix:- (1084:tqdm_logging.py:145)
11:12:52.897 [I] Progress on: 94.0it/1.50kit rate:7.0s/it remaining:2:45:01 elapsed:15:08 postfix:- (1084:tqdm_logging.py:145)
11:13:07.016 [I] Progress on: 96.0it/1.50kit rate:7.0s/it remaining:2:44:57 elapsed:15:22 postfix:- (1084:tqdm_logging.py:145)
11:13:21.157 [I] Progress on: 98.0it/1.50kit rate:7.1s/it remaining:2:44:59 elapsed:15:36 postfix:- (1084:tqdm_logging.py:145)
11:13:35.225 [I] Progress on: 100it/1.50kit rate:7.0s/it remaining:2:44:27 elapsed:15:50 postfix:- (1084:tqdm_logging.py:145)


Step 100: grad_norm=0.0495, loss=0.0048, param_norm=1804.2166
11:13:49.001 [I] Progress on: 100it/1.50kit rate:7.0s/it remaining:2:44:27 elapsed:16:04 postfix:- (1084:tqdm_logging.py:145)
11:14:03.535 [I] Progress on: 104it/1.50kit rate:6.8s/it remaining:2:37:30 elapsed:16:18 postfix:- (1084:tqdm_logging.py:145)
11:14:17.641 [I] Progress on: 106it/1.50kit rate:6.9s/it remaining:2:40:38 elapsed:16:32 postfix:- (1084:tqdm_logging.py:145)
11:14:31.723 [I] Progress on: 108it/1.50kit rate:7.0s/it remaining:2:41:53 elapsed:16:46 postfix:- (1084:tqdm_logging.py:145)
11:14:45.848 [I] Progress on: 110it/1.50kit rate:7.0s/it remaining:2:42:40 elapsed:17:01 postfix:- (1084:tqdm_logging.py:145)
11:14:59.941 [I] Progress on: 112it/1.50kit rate:7.0s/it remaining:2:42:44 elapsed:17:15 postfix:- (1084:tqdm_logging.py:145)
11:15:14.053 [I] Progress on: 114it/1.50kit rate:7.0s/it remaining:2:42:43 elapsed:17:29 postfix:- (1084:tqdm_logging.py:145)
11:15:28.160 [I] Progress on: 116it/1.50kit rate:7.0s/it remaining:2:42:35 elapsed:17:43 postfix:- (1084:tqdm_logging.py:145)
11:15:42.261 [I] Progress on: 118it/1.50kit rate:7.1s/it remaining:2:42:23 elapsed:17:57 postfix:- (1084:tqdm_logging.py:145)
11:15:56.346 [I] Progress on: 120it/1.50kit rate:7.0s/it remaining:2:42:03 elapsed:18:11 postfix:- (1084:tqdm_logging.py:145)
11:16:10.433 [I] Progress on: 122it/1.50kit rate:7.0s/it remaining:2:41:47 elapsed:18:25 postfix:- (1084:tqdm_logging.py:145)
11:16:24.544 [I] Progress on: 124it/1.50kit rate:7.0s/it remaining:2:41:38 elapsed:18:39 postfix:- (1084:tqdm_logging.py:145)
11:16:38.640 [I] Progress on: 126it/1.50kit rate:7.0s/it remaining:2:41:23 elapsed:18:53 postfix:- (1084:tqdm_logging.py:145)
11:16:52.746 [I] Progress on: 128it/1.50kit rate:7.1s/it remaining:2:41:14 elapsed:19:07 postfix:- (1084:tqdm_logging.py:145)
11:17:06.851 [I] Progress on: 130it/1.50kit rate:7.1s/it remaining:2:41:02 elapsed:19:22 postfix:- (1084:tqdm_logging.py:145)
11:17:20.984 [I] Progress on: 132it/1.50kit rate:7.1s/it remaining:2:40:59 elapsed:19:36 postfix:- (1084:tqdm_logging.py:145)
11:17:35.626 [I] Progress on: 134it/1.50kit rate:7.2s/it remaining:2:44:22 elapsed:19:50 postfix:- (1084:tqdm_logging.py:145)
11:17:49.203 [I] Progress on: 136it/1.50kit rate:7.0s/it remaining:2:39:45 elapsed:20:04 postfix:- (1084:tqdm_logging.py:145)
11:18:03.343 [I] Progress on: 138it/1.50kit rate:7.1s/it remaining:2:40:06 elapsed:20:18 postfix:- (1084:tqdm_logging.py:145)
11:18:17.458 [I] Progress on: 140it/1.50kit rate:7.1s/it remaining:2:40:01 elapsed:20:32 postfix:- (1084:tqdm_logging.py:145)
11:18:31.569 [I] Progress on: 142it/1.50kit rate:7.1s/it remaining:2:39:50 elapsed:20:46 postfix:- (1084:tqdm_logging.py:145)
11:18:45.683 [I] Progress on: 144it/1.50kit rate:7.1s/it remaining:2:39:39 elapsed:21:00 postfix:- (1084:tqdm_logging.py:145)
11:18:59.782 [I] Progress on: 146it/1.50kit rate:7.1s/it remaining:2:39:22 elapsed:21:14 postfix:- (1084:tqdm_logging.py:145)
11:19:13.871 [I] Progress on: 148it/1.50kit rate:7.1s/it remaining:2:39:01 elapsed:21:29 postfix:- (1084:tqdm_logging.py:145)
11:19:27.988 [I] Progress on: 150it/1.50kit rate:7.1s/it remaining:2:38:55 elapsed:21:43 postfix:- (1084:tqdm_logging.py:145)


Step 150: grad_norm=0.0530, loss=0.0051, param_norm=1804.2188
11:19:41.734 [I] Progress on: 150it/1.50kit rate:7.1s/it remaining:2:38:55 elapsed:21:56 postfix:- (1084:tqdm_logging.py:145)
11:19:56.272 [I] Progress on: 154it/1.50kit rate:6.8s/it remaining:2:31:51 elapsed:22:11 postfix:- (1084:tqdm_logging.py:145)
11:20:10.357 [I] Progress on: 156it/1.50kit rate:6.9s/it remaining:2:34:45 elapsed:22:25 postfix:- (1084:tqdm_logging.py:145)
11:20:24.445 [I] Progress on: 158it/1.50kit rate:7.0s/it remaining:2:36:04 elapsed:22:39 postfix:- (1084:tqdm_logging.py:145)
11:20:38.529 [I] Progress on: 160it/1.50kit rate:7.0s/it remaining:2:36:31 elapsed:22:53 postfix:- (1084:tqdm_logging.py:145)
11:20:52.619 [I] Progress on: 162it/1.50kit rate:7.0s/it remaining:2:36:41 elapsed:23:07 postfix:- (1084:tqdm_logging.py:145)
11:21:06.723 [I] Progress on: 164it/1.50kit rate:7.0s/it remaining:2:36:45 elapsed:23:21 postfix:- (1084:tqdm_logging.py:145)
11:21:20.821 [I] Progress on: 166it/1.50kit rate:7.0s/it remaining:2:36:36 elapsed:23:36 postfix:- (1084:tqdm_logging.py:145)
11:21:34.923 [I] Progress on: 168it/1.50kit rate:7.0s/it remaining:2:36:26 elapsed:23:50 postfix:- (1084:tqdm_logging.py:145)
11:21:49.016 [I] Progress on: 170it/1.50kit rate:7.0s/it remaining:2:36:14 elapsed:24:04 postfix:- (1084:tqdm_logging.py:145)
11:22:03.086 [I] Progress on: 172it/1.50kit rate:7.0s/it remaining:2:35:51 elapsed:24:18 postfix:- (1084:tqdm_logging.py:145)
11:22:17.171 [I] Progress on: 174it/1.50kit rate:7.0s/it remaining:2:35:36 elapsed:24:32 postfix:- (1084:tqdm_logging.py:145)
11:22:31.260 [I] Progress on: 176it/1.50kit rate:7.0s/it remaining:2:35:24 elapsed:24:46 postfix:- (1084:tqdm_logging.py:145)
11:22:45.334 [I] Progress on: 178it/1.50kit rate:7.0s/it remaining:2:35:06 elapsed:25:00 postfix:- (1084:tqdm_logging.py:145)
11:22:59.410 [I] Progress on: 180it/1.50kit rate:7.0s/it remaining:2:34:51 elapsed:25:14 postfix:- (1084:tqdm_logging.py:145)
11:23:13.504 [I] Progress on: 182it/1.50kit rate:7.0s/it remaining:2:34:40 elapsed:25:28 postfix:- (1084:tqdm_logging.py:145)
11:23:27.603 [I] Progress on: 184it/1.50kit rate:7.0s/it remaining:2:34:31 elapsed:25:42 postfix:- (1084:tqdm_logging.py:145)
11:23:41.685 [I] Progress on: 186it/1.50kit rate:7.0s/it remaining:2:34:15 elapsed:25:56 postfix:- (1084:tqdm_logging.py:145)
11:23:55.770 [I] Progress on: 188it/1.50kit rate:7.0s/it remaining:2:33:59 elapsed:26:10 postfix:- (1084:tqdm_logging.py:145)
11:24:09.868 [I] Progress on: 190it/1.50kit rate:7.0s/it remaining:2:33:49 elapsed:26:25 postfix:- (1084:tqdm_logging.py:145)
11:24:23.972 [I] Progress on: 192it/1.50kit rate:7.0s/it remaining:2:33:40 elapsed:26:39 postfix:- (1084:tqdm_logging.py:145)
11:24:38.060 [I] Progress on: 194it/1.50kit rate:7.0s/it remaining:2:33:24 elapsed:26:53 postfix:- (1084:tqdm_logging.py:145)
11:24:52.160 [I] Progress on: 196it/1.50kit rate:7.0s/it remaining:2:33:11 elapsed:27:07 postfix:- (1084:tqdm_logging.py:145)
11:25:06.260 [I] Progress on: 198it/1.50kit rate:7.0s/it remaining:2:32:59 elapsed:27:21 postfix:- (1084:tqdm_logging.py:145)
11:25:20.375 [I] Progress on: 200it/1.50kit rate:7.1s/it remaining:2:32:48 elapsed:27:35 postfix:- (1084:tqdm_logging.py:145)


Step 200: grad_norm=0.0513, loss=0.0052, param_norm=1804.2212
11:25:34.115 [I] Progress on: 200it/1.50kit rate:7.1s/it remaining:2:32:48 elapsed:27:49 postfix:- (1084:tqdm_logging.py:145)
11:25:48.636 [I] Progress on: 204it/1.50kit rate:6.8s/it remaining:2:26:00 elapsed:28:03 postfix:- (1084:tqdm_logging.py:145)
11:26:02.717 [I] Progress on: 206it/1.50kit rate:6.9s/it remaining:2:28:51 elapsed:28:17 postfix:- (1084:tqdm_logging.py:145)
11:26:16.813 [I] Progress on: 208it/1.50kit rate:7.0s/it remaining:2:30:13 elapsed:28:32 postfix:- (1084:tqdm_logging.py:145)
11:26:30.914 [I] Progress on: 210it/1.50kit rate:7.0s/it remaining:2:30:49 elapsed:28:46 postfix:- (1084:tqdm_logging.py:145)
11:26:44.998 [I] Progress on: 212it/1.50kit rate:7.0s/it remaining:2:30:54 elapsed:29:00 postfix:- (1084:tqdm_logging.py:145)
11:26:59.093 [I] Progress on: 214it/1.50kit rate:7.0s/it remaining:2:30:52 elapsed:29:14 postfix:- (1084:tqdm_logging.py:145)
11:27:13.176 [I] Progress on: 216it/1.50kit rate:7.0s/it remaining:2:30:39 elapsed:29:28 postfix:- (1084:tqdm_logging.py:145)
11:27:27.271 [I] Progress on: 218it/1.50kit rate:7.0s/it remaining:2:30:27 elapsed:29:42 postfix:- (1084:tqdm_logging.py:145)
11:27:41.366 [I] Progress on: 220it/1.50kit rate:7.0s/it remaining:2:30:15 elapsed:29:56 postfix:- (1084:tqdm_logging.py:145)
11:27:55.469 [I] Progress on: 222it/1.50kit rate:7.0s/it remaining:2:30:04 elapsed:30:10 postfix:- (1084:tqdm_logging.py:145)
11:28:09.558 [I] Progress on: 224it/1.50kit rate:7.0s/it remaining:2:29:49 elapsed:30:24 postfix:- (1084:tqdm_logging.py:145)
11:28:23.644 [I] Progress on: 226it/1.50kit rate:7.0s/it remaining:2:29:34 elapsed:30:38 postfix:- (1084:tqdm_logging.py:145)
11:28:37.773 [I] Progress on: 228it/1.50kit rate:7.1s/it remaining:2:29:34 elapsed:30:52 postfix:- (1084:tqdm_logging.py:145)
11:28:51.880 [I] Progress on: 230it/1.50kit rate:7.1s/it remaining:2:29:20 elapsed:31:07 postfix:- (1084:tqdm_logging.py:145)
11:29:05.989 [I] Progress on: 232it/1.50kit rate:7.1s/it remaining:2:29:06 elapsed:31:21 postfix:- (1084:tqdm_logging.py:145)
11:29:20.105 [I] Progress on: 234it/1.50kit rate:7.1s/it remaining:2:28:54 elapsed:31:35 postfix:- (1084:tqdm_logging.py:145)
11:29:34.216 [I] Progress on: 236it/1.50kit rate:7.1s/it remaining:2:28:38 elapsed:31:49 postfix:- (1084:tqdm_logging.py:145)
11:29:48.362 [I] Progress on: 238it/1.50kit rate:7.1s/it remaining:2:28:34 elapsed:32:03 postfix:- (1084:tqdm_logging.py:145)
11:30:02.444 [I] Progress on: 240it/1.50kit rate:7.1s/it remaining:2:28:06 elapsed:32:17 postfix:- (1084:tqdm_logging.py:145)
11:30:16.607 [I] Progress on: 242it/1.50kit rate:7.1s/it remaining:2:28:11 elapsed:32:31 postfix:- (1084:tqdm_logging.py:145)
11:30:30.678 [I] Progress on: 244it/1.50kit rate:7.1s/it remaining:2:27:37 elapsed:32:45 postfix:- (1084:tqdm_logging.py:145)
11:30:44.802 [I] Progress on: 246it/1.50kit rate:7.1s/it remaining:2:27:29 elapsed:32:59 postfix:- (1084:tqdm_logging.py:145)
11:30:58.915 [I] Progress on: 248it/1.50kit rate:7.1s/it remaining:2:27:15 elapsed:33:14 postfix:- (1084:tqdm_logging.py:145)
11:31:12.993 [I] Progress on: 250it/1.50kit rate:7.0s/it remaining:2:26:48 elapsed:33:28 postfix:- (1084:tqdm_logging.py:145)


Step 250: grad_norm=0.0536, loss=0.0045, param_norm=1804.2238
11:31:26.757 [I] Progress on: 250it/1.50kit rate:7.0s/it remaining:2:26:48 elapsed:33:41 postfix:- (1084:tqdm_logging.py:145)
11:31:41.275 [I] Progress on: 254it/1.50kit rate:6.8s/it remaining:2:20:31 elapsed:33:56 postfix:- (1084:tqdm_logging.py:145)
11:31:55.381 [I] Progress on: 256it/1.50kit rate:6.9s/it remaining:2:23:20 elapsed:34:10 postfix:- (1084:tqdm_logging.py:145)
11:32:09.482 [I] Progress on: 258it/1.50kit rate:7.0s/it remaining:2:24:33 elapsed:34:24 postfix:- (1084:tqdm_logging.py:145)
11:32:23.599 [I] Progress on: 260it/1.50kit rate:7.0s/it remaining:2:25:05 elapsed:34:38 postfix:- (1084:tqdm_logging.py:145)
11:32:37.702 [I] Progress on: 262it/1.50kit rate:7.0s/it remaining:2:25:09 elapsed:34:52 postfix:- (1084:tqdm_logging.py:145)
11:32:51.828 [I] Progress on: 264it/1.50kit rate:7.0s/it remaining:2:25:10 elapsed:35:07 postfix:- (1084:tqdm_logging.py:145)
11:33:05.957 [I] Progress on: 266it/1.50kit rate:7.1s/it remaining:2:25:07 elapsed:35:21 postfix:- (1084:tqdm_logging.py:145)
11:33:20.606 [I] Progress on: 268it/1.50kit rate:7.2s/it remaining:2:28:11 elapsed:35:35 postfix:- (1084:tqdm_logging.py:145)
11:33:34.178 [I] Progress on: 270it/1.50kit rate:7.0s/it remaining:2:23:59 elapsed:35:49 postfix:- (1084:tqdm_logging.py:145)
11:33:48.297 [I] Progress on: 272it/1.50kit rate:7.0s/it remaining:2:24:08 elapsed:36:03 postfix:- (1084:tqdm_logging.py:145)
11:34:02.415 [I] Progress on: 274it/1.50kit rate:7.1s/it remaining:2:24:04 elapsed:36:17 postfix:- (1084:tqdm_logging.py:145)
11:34:16.515 [I] Progress on: 276it/1.50kit rate:7.0s/it remaining:2:23:48 elapsed:36:31 postfix:- (1084:tqdm_logging.py:145)
11:34:30.621 [I] Progress on: 278it/1.50kit rate:7.1s/it remaining:2:23:35 elapsed:36:45 postfix:- (1084:tqdm_logging.py:145)
11:34:44.723 [I] Progress on: 280it/1.50kit rate:7.1s/it remaining:2:23:23 elapsed:36:59 postfix:- (1084:tqdm_logging.py:145)
11:34:58.811 [I] Progress on: 282it/1.50kit rate:7.0s/it remaining:2:23:04 elapsed:37:14 postfix:- (1084:tqdm_logging.py:145)
11:35:12.911 [I] Progress on: 284it/1.50kit rate:7.0s/it remaining:2:22:50 elapsed:37:28 postfix:- (1084:tqdm_logging.py:145)
11:35:27.044 [I] Progress on: 286it/1.50kit rate:7.1s/it remaining:2:22:47 elapsed:37:42 postfix:- (1084:tqdm_logging.py:145)
11:35:41.133 [I] Progress on: 288it/1.50kit rate:7.1s/it remaining:2:22:25 elapsed:37:56 postfix:- (1084:tqdm_logging.py:145)
11:35:55.231 [I] Progress on: 290it/1.50kit rate:7.0s/it remaining:2:22:09 elapsed:38:10 postfix:- (1084:tqdm_logging.py:145)
11:36:09.314 [I] Progress on: 292it/1.50kit rate:7.0s/it remaining:2:21:50 elapsed:38:24 postfix:- (1084:tqdm_logging.py:145)
11:36:23.438 [I] Progress on: 294it/1.50kit rate:7.1s/it remaining:2:21:47 elapsed:38:38 postfix:- (1084:tqdm_logging.py:145)
11:36:37.541 [I] Progress on: 296it/1.50kit rate:7.1s/it remaining:2:21:30 elapsed:38:52 postfix:- (1084:tqdm_logging.py:145)
11:36:51.641 [I] Progress on: 298it/1.50kit rate:7.1s/it remaining:2:21:17 elapsed:39:06 postfix:- (1084:tqdm_logging.py:145)
11:37:05.705 [I] Progress on: 300it/1.50kit rate:7.0s/it remaining:2:20:50 elapsed:39:20 postfix:- (1084:tqdm_logging.py:145)


Step 300: grad_norm=0.0467, loss=0.0044, param_norm=1804.2260
11:37:19.438 [I] Progress on: 300it/1.50kit rate:7.0s/it remaining:2:20:50 elapsed:39:34 postfix:- (1084:tqdm_logging.py:145)
11:37:33.920 [I] Progress on: 304it/1.50kit rate:6.8s/it remaining:2:14:34 elapsed:39:49 postfix:- (1084:tqdm_logging.py:145)
11:37:48.022 [I] Progress on: 306it/1.50kit rate:6.9s/it remaining:2:17:26 elapsed:40:03 postfix:- (1084:tqdm_logging.py:145)
11:38:02.136 [I] Progress on: 308it/1.50kit rate:7.0s/it remaining:2:18:46 elapsed:40:17 postfix:- (1084:tqdm_logging.py:145)
11:38:16.206 [I] Progress on: 310it/1.50kit rate:7.0s/it remaining:2:19:04 elapsed:40:31 postfix:- (1084:tqdm_logging.py:145)
11:38:30.280 [I] Progress on: 312it/1.50kit rate:7.0s/it remaining:2:19:07 elapsed:40:45 postfix:- (1084:tqdm_logging.py:145)
11:38:44.366 [I] Progress on: 314it/1.50kit rate:7.0s/it remaining:2:19:04 elapsed:40:59 postfix:- (1084:tqdm_logging.py:145)
11:38:58.446 [I] Progress on: 316it/1.50kit rate:7.0s/it remaining:2:18:51 elapsed:41:13 postfix:- (1084:tqdm_logging.py:145)
11:39:12.551 [I] Progress on: 318it/1.50kit rate:7.0s/it remaining:2:18:47 elapsed:41:27 postfix:- (1084:tqdm_logging.py:145)
11:39:26.644 [I] Progress on: 320it/1.50kit rate:7.0s/it remaining:2:18:33 elapsed:41:41 postfix:- (1084:tqdm_logging.py:145)
11:39:40.764 [I] Progress on: 322it/1.50kit rate:7.1s/it remaining:2:18:28 elapsed:41:55 postfix:- (1084:tqdm_logging.py:145)
11:39:54.858 [I] Progress on: 324it/1.50kit rate:7.1s/it remaining:2:18:12 elapsed:42:10 postfix:- (1084:tqdm_logging.py:145)
11:40:08.962 [I] Progress on: 326it/1.50kit rate:7.1s/it remaining:2:17:58 elapsed:42:24 postfix:- (1084:tqdm_logging.py:145)
11:40:23.083 [I] Progress on: 328it/1.50kit rate:7.1s/it remaining:2:17:51 elapsed:42:38 postfix:- (1084:tqdm_logging.py:145)
11:40:37.180 [I] Progress on: 330it/1.50kit rate:7.1s/it remaining:2:17:31 elapsed:42:52 postfix:- (1084:tqdm_logging.py:145)
11:40:51.281 [I] Progress on: 332it/1.50kit rate:7.1s/it remaining:2:17:17 elapsed:43:06 postfix:- (1084:tqdm_logging.py:145)
11:41:05.370 [I] Progress on: 334it/1.50kit rate:7.0s/it remaining:2:16:56 elapsed:43:20 postfix:- (1084:tqdm_logging.py:145)
11:41:19.496 [I] Progress on: 336it/1.50kit rate:7.1s/it remaining:2:16:50 elapsed:43:34 postfix:- (1084:tqdm_logging.py:145)
11:41:33.559 [I] Progress on: 338it/1.50kit rate:7.0s/it remaining:2:16:24 elapsed:43:48 postfix:- (1084:tqdm_logging.py:145)
11:41:47.663 [I] Progress on: 340it/1.50kit rate:7.0s/it remaining:2:16:15 elapsed:44:02 postfix:- (1084:tqdm_logging.py:145)
11:42:01.799 [I] Progress on: 342it/1.50kit rate:7.1s/it remaining:2:16:12 elapsed:44:16 postfix:- (1084:tqdm_logging.py:145)
11:42:15.946 [I] Progress on: 344it/1.50kit rate:7.1s/it remaining:2:16:07 elapsed:44:31 postfix:- (1084:tqdm_logging.py:145)
11:42:30.024 [I] Progress on: 346it/1.50kit rate:7.1s/it remaining:2:15:36 elapsed:44:45 postfix:- (1084:tqdm_logging.py:145)
11:42:44.100 [I] Progress on: 348it/1.50kit rate:7.0s/it remaining:2:15:16 elapsed:44:59 postfix:- (1084:tqdm_logging.py:145)
11:42:58.219 [I] Progress on: 350it/1.50kit rate:7.1s/it remaining:2:15:11 elapsed:45:13 postfix:- (1084:tqdm_logging.py:145)


Step 350: grad_norm=0.0481, loss=0.0042, param_norm=1804.2288
11:43:11.982 [I] Progress on: 350it/1.50kit rate:7.1s/it remaining:2:15:11 elapsed:45:27 postfix:- (1084:tqdm_logging.py:145)
11:43:26.521 [I] Progress on: 354it/1.50kit rate:6.8s/it remaining:2:09:09 elapsed:45:41 postfix:- (1084:tqdm_logging.py:145)
11:43:40.619 [I] Progress on: 356it/1.50kit rate:6.9s/it remaining:2:11:43 elapsed:45:55 postfix:- (1084:tqdm_logging.py:145)
11:43:54.712 [I] Progress on: 358it/1.50kit rate:7.0s/it remaining:2:12:49 elapsed:46:09 postfix:- (1084:tqdm_logging.py:145)
11:44:08.789 [I] Progress on: 360it/1.50kit rate:7.0s/it remaining:2:13:09 elapsed:46:23 postfix:- (1084:tqdm_logging.py:145)
11:44:22.869 [I] Progress on: 362it/1.50kit rate:7.0s/it remaining:2:13:12 elapsed:46:38 postfix:- (1084:tqdm_logging.py:145)
11:44:36.982 [I] Progress on: 364it/1.50kit rate:7.0s/it remaining:2:13:16 elapsed:46:52 postfix:- (1084:tqdm_logging.py:145)
11:44:51.075 [I] Progress on: 366it/1.50kit rate:7.0s/it remaining:2:13:04 elapsed:47:06 postfix:- (1084:tqdm_logging.py:145)
11:45:05.173 [I] Progress on: 368it/1.50kit rate:7.0s/it remaining:2:12:54 elapsed:47:20 postfix:- (1084:tqdm_logging.py:145)
11:45:19.284 [I] Progress on: 370it/1.50kit rate:7.0s/it remaining:2:12:44 elapsed:47:34 postfix:- (1084:tqdm_logging.py:145)
11:45:33.398 [I] Progress on: 372it/1.50kit rate:7.1s/it remaining:2:12:36 elapsed:47:48 postfix:- (1084:tqdm_logging.py:145)
11:45:47.487 [I] Progress on: 374it/1.50kit rate:7.1s/it remaining:2:12:18 elapsed:48:02 postfix:- (1084:tqdm_logging.py:145)
11:46:01.601 [I] Progress on: 376it/1.50kit rate:7.1s/it remaining:2:12:07 elapsed:48:16 postfix:- (1084:tqdm_logging.py:145)
11:46:15.697 [I] Progress on: 378it/1.50kit rate:7.1s/it remaining:2:11:50 elapsed:48:30 postfix:- (1084:tqdm_logging.py:145)
11:46:29.831 [I] Progress on: 380it/1.50kit rate:7.1s/it remaining:2:11:47 elapsed:48:45 postfix:- (1084:tqdm_logging.py:145)
11:46:43.939 [I] Progress on: 382it/1.50kit rate:7.1s/it remaining:2:11:30 elapsed:48:59 postfix:- (1084:tqdm_logging.py:145)
11:46:58.058 [I] Progress on: 384it/1.50kit rate:7.1s/it remaining:2:11:17 elapsed:49:13 postfix:- (1084:tqdm_logging.py:145)
11:47:12.160 [I] Progress on: 386it/1.50kit rate:7.1s/it remaining:2:10:56 elapsed:49:27 postfix:- (1084:tqdm_logging.py:145)
11:47:26.271 [I] Progress on: 388it/1.50kit rate:7.1s/it remaining:2:10:41 elapsed:49:41 postfix:- (1084:tqdm_logging.py:145)
11:47:40.370 [I] Progress on: 390it/1.50kit rate:7.1s/it remaining:2:10:27 elapsed:49:55 postfix:- (1084:tqdm_logging.py:145)
11:47:54.462 [I] Progress on: 392it/1.50kit rate:7.0s/it remaining:2:10:09 elapsed:50:09 postfix:- (1084:tqdm_logging.py:145)
11:48:08.588 [I] Progress on: 394it/1.50kit rate:7.1s/it remaining:2:10:03 elapsed:50:23 postfix:- (1084:tqdm_logging.py:145)
11:48:22.670 [I] Progress on: 396it/1.50kit rate:7.1s/it remaining:2:09:43 elapsed:50:37 postfix:- (1084:tqdm_logging.py:145)
11:48:36.770 [I] Progress on: 398it/1.50kit rate:7.1s/it remaining:2:09:30 elapsed:50:51 postfix:- (1084:tqdm_logging.py:145)
11:48:50.853 [I] Progress on: 400it/1.50kit rate:7.0s/it remaining:2:09:12 elapsed:51:06 postfix:- (1084:tqdm_logging.py:145)


Step 400: grad_norm=0.0463, loss=0.0039, param_norm=1804.2307
11:49:04.612 [I] Progress on: 400it/1.50kit rate:7.0s/it remaining:2:09:12 elapsed:51:19 postfix:- (1084:tqdm_logging.py:145)
11:49:19.225 [I] Progress on: 404it/1.50kit rate:6.7s/it remaining:2:03:08 elapsed:51:34 postfix:- (1084:tqdm_logging.py:145)
11:49:33.354 [I] Progress on: 406it/1.50kit rate:6.9s/it remaining:2:06:02 elapsed:51:48 postfix:- (1084:tqdm_logging.py:145)
11:49:47.423 [I] Progress on: 408it/1.50kit rate:7.0s/it remaining:2:07:03 elapsed:52:02 postfix:- (1084:tqdm_logging.py:145)
11:50:01.568 [I] Progress on: 410it/1.50kit rate:7.0s/it remaining:2:07:46 elapsed:52:16 postfix:- (1084:tqdm_logging.py:145)
11:50:15.688 [I] Progress on: 412it/1.50kit rate:7.1s/it remaining:2:07:51 elapsed:52:30 postfix:- (1084:tqdm_logging.py:145)
11:50:29.825 [I] Progress on: 414it/1.50kit rate:7.1s/it remaining:2:07:53 elapsed:52:45 postfix:- (1084:tqdm_logging.py:145)
11:50:43.971 [I] Progress on: 416it/1.50kit rate:7.1s/it remaining:2:07:48 elapsed:52:59 postfix:- (1084:tqdm_logging.py:145)
11:50:58.106 [I] Progress on: 418it/1.50kit rate:7.1s/it remaining:2:07:35 elapsed:53:13 postfix:- (1084:tqdm_logging.py:145)
11:51:12.252 [I] Progress on: 420it/1.50kit rate:7.1s/it remaining:2:07:24 elapsed:53:27 postfix:- (1084:tqdm_logging.py:145)
11:51:26.418 [I] Progress on: 422it/1.50kit rate:7.1s/it remaining:2:07:18 elapsed:53:41 postfix:- (1084:tqdm_logging.py:145)
11:51:40.556 [I] Progress on: 424it/1.50kit rate:7.1s/it remaining:2:06:59 elapsed:53:55 postfix:- (1084:tqdm_logging.py:145)
11:51:54.697 [I] Progress on: 426it/1.50kit rate:7.1s/it remaining:2:06:44 elapsed:54:09 postfix:- (1084:tqdm_logging.py:145)
11:52:08.851 [I] Progress on: 428it/1.50kit rate:7.1s/it remaining:2:06:32 elapsed:54:24 postfix:- (1084:tqdm_logging.py:145)
11:52:22.995 [I] Progress on: 430it/1.50kit rate:7.1s/it remaining:2:06:17 elapsed:54:38 postfix:- (1084:tqdm_logging.py:145)
11:52:37.149 [I] Progress on: 432it/1.50kit rate:7.1s/it remaining:2:06:04 elapsed:54:52 postfix:- (1084:tqdm_logging.py:145)
11:52:51.316 [I] Progress on: 434it/1.50kit rate:7.1s/it remaining:2:05:53 elapsed:55:06 postfix:- (1084:tqdm_logging.py:145)
11:53:05.484 [I] Progress on: 436it/1.50kit rate:7.1s/it remaining:2:05:43 elapsed:55:20 postfix:- (1084:tqdm_logging.py:145)
11:53:19.645 [I] Progress on: 438it/1.50kit rate:7.1s/it remaining:2:05:30 elapsed:55:34 postfix:- (1084:tqdm_logging.py:145)
11:53:33.797 [I] Progress on: 440it/1.50kit rate:7.1s/it remaining:2:05:13 elapsed:55:48 postfix:- (1084:tqdm_logging.py:145)
11:53:47.949 [I] Progress on: 442it/1.50kit rate:7.1s/it remaining:2:04:58 elapsed:56:03 postfix:- (1084:tqdm_logging.py:145)
11:54:02.077 [I] Progress on: 444it/1.50kit rate:7.1s/it remaining:2:04:37 elapsed:56:17 postfix:- (1084:tqdm_logging.py:145)
11:54:16.235 [I] Progress on: 446it/1.50kit rate:7.1s/it remaining:2:04:27 elapsed:56:31 postfix:- (1084:tqdm_logging.py:145)
11:54:30.323 [I] Progress on: 448it/1.50kit rate:7.1s/it remaining:2:03:54 elapsed:56:45 postfix:- (1084:tqdm_logging.py:145)
11:54:44.424 [I] Progress on: 450it/1.50kit rate:7.1s/it remaining:2:03:31 elapsed:56:59 postfix:- (1084:tqdm_logging.py:145)


Step 450: grad_norm=0.0525, loss=0.0043, param_norm=1804.2330
11:54:58.194 [I] Progress on: 450it/1.50kit rate:7.1s/it remaining:2:03:31 elapsed:57:13 postfix:- (1084:tqdm_logging.py:145)
11:55:12.776 [I] Progress on: 454it/1.50kit rate:6.8s/it remaining:1:58:10 elapsed:57:27 postfix:- (1084:tqdm_logging.py:145)
11:55:26.897 [I] Progress on: 456it/1.50kit rate:6.9s/it remaining:2:00:26 elapsed:57:42 postfix:- (1084:tqdm_logging.py:145)
11:55:41.012 [I] Progress on: 458it/1.50kit rate:7.0s/it remaining:2:01:23 elapsed:57:56 postfix:- (1084:tqdm_logging.py:145)
11:55:55.125 [I] Progress on: 460it/1.50kit rate:7.0s/it remaining:2:01:44 elapsed:58:10 postfix:- (1084:tqdm_logging.py:145)
11:56:09.239 [I] Progress on: 462it/1.50kit rate:7.0s/it remaining:2:01:47 elapsed:58:24 postfix:- (1084:tqdm_logging.py:145)
11:56:23.352 [I] Progress on: 464it/1.50kit rate:7.0s/it remaining:2:01:41 elapsed:58:38 postfix:- (1084:tqdm_logging.py:145)
11:56:37.468 [I] Progress on: 466it/1.50kit rate:7.1s/it remaining:2:01:32 elapsed:58:52 postfix:- (1084:tqdm_logging.py:145)
11:56:51.603 [I] Progress on: 468it/1.50kit rate:7.1s/it remaining:2:01:26 elapsed:59:06 postfix:- (1084:tqdm_logging.py:145)
11:57:05.699 [I] Progress on: 470it/1.50kit rate:7.1s/it remaining:2:01:06 elapsed:59:20 postfix:- (1084:tqdm_logging.py:145)
11:57:19.801 [I] Progress on: 472it/1.50kit rate:7.1s/it remaining:2:00:50 elapsed:59:34 postfix:- (1084:tqdm_logging.py:145)
11:57:33.931 [I] Progress on: 474it/1.50kit rate:7.1s/it remaining:2:00:42 elapsed:59:49 postfix:- (1084:tqdm_logging.py:145)
11:57:48.051 [I] Progress on: 476it/1.50kit rate:7.1s/it remaining:2:00:28 elapsed:1:00:03 postfix:- (1084:tqdm_logging.py:145)
11:58:02.171 [I] Progress on: 478it/1.50kit rate:7.1s/it remaining:2:00:15 elapsed:1:00:17 postfix:- (1084:tqdm_logging.py:145)
11:58:16.289 [I] Progress on: 480it/1.50kit rate:7.1s/it remaining:2:00:00 elapsed:1:00:31 postfix:- (1084:tqdm_logging.py:145)
11:58:30.412 [I] Progress on: 482it/1.50kit rate:7.1s/it remaining:1:59:48 elapsed:1:00:45 postfix:- (1084:tqdm_logging.py:145)
11:58:44.516 [I] Progress on: 484it/1.50kit rate:7.1s/it remaining:1:59:29 elapsed:1:00:59 postfix:- (1084:tqdm_logging.py:145)
11:58:58.632 [I] Progress on: 486it/1.50kit rate:7.1s/it remaining:1:59:16 elapsed:1:01:13 postfix:- (1084:tqdm_logging.py:145)
11:59:12.755 [I] Progress on: 488it/1.50kit rate:7.1s/it remaining:1:59:04 elapsed:1:01:27 postfix:- (1084:tqdm_logging.py:145)
11:59:26.898 [I] Progress on: 490it/1.50kit rate:7.1s/it remaining:1:58:55 elapsed:1:01:42 postfix:- (1084:tqdm_logging.py:145)
11:59:40.988 [I] Progress on: 492it/1.50kit rate:7.1s/it remaining:1:58:29 elapsed:1:01:56 postfix:- (1084:tqdm_logging.py:145)
11:59:55.111 [I] Progress on: 494it/1.50kit rate:7.1s/it remaining:1:58:19 elapsed:1:02:10 postfix:- (1084:tqdm_logging.py:145)
12:00:09.233 [I] Progress on: 496it/1.50kit rate:7.1s/it remaining:1:58:07 elapsed:1:02:24 postfix:- (1084:tqdm_logging.py:145)
12:00:23.354 [I] Progress on: 498it/1.50kit rate:7.1s/it remaining:1:57:54 elapsed:1:02:38 postfix:- (1084:tqdm_logging.py:145)
12:00:37.453 [I] Progress on: 500it/1.50kit rate:7.1s/it remaining:1:57:35 elapsed:1:02:52 postfix:- (1084:tqdm_logging.py:145)


Step 500: grad_norm=0.0476, loss=0.0041, param_norm=1804.2352
12:00:51.210 [I] Progress on: 500it/1.50kit rate:7.1s/it remaining:1:57:35 elapsed:1:03:06 postfix:- (1084:tqdm_logging.py:145)
12:01:05.722 [I] Progress on: 504it/1.50kit rate:6.8s/it remaining:1:52:18 elapsed:1:03:20 postfix:- (1084:tqdm_logging.py:145)
12:01:19.849 [I] Progress on: 506it/1.50kit rate:6.9s/it remaining:1:54:36 elapsed:1:03:35 postfix:- (1084:tqdm_logging.py:145)
12:01:33.968 [I] Progress on: 508it/1.50kit rate:7.0s/it remaining:1:55:35 elapsed:1:03:49 postfix:- (1084:tqdm_logging.py:145)
12:01:48.068 [I] Progress on: 510it/1.50kit rate:7.0s/it remaining:1:55:51 elapsed:1:04:03 postfix:- (1084:tqdm_logging.py:145)
12:02:02.192 [I] Progress on: 512it/1.50kit rate:7.0s/it remaining:1:55:57 elapsed:1:04:17 postfix:- (1084:tqdm_logging.py:145)
12:02:16.299 [I] Progress on: 514it/1.50kit rate:7.0s/it remaining:1:55:48 elapsed:1:04:31 postfix:- (1084:tqdm_logging.py:145)
12:02:30.418 [I] Progress on: 516it/1.50kit rate:7.1s/it remaining:1:55:40 elapsed:1:04:45 postfix:- (1084:tqdm_logging.py:145)
12:02:44.540 [I] Progress on: 518it/1.50kit rate:7.1s/it remaining:1:55:29 elapsed:1:04:59 postfix:- (1084:tqdm_logging.py:145)
12:02:58.668 [I] Progress on: 520it/1.50kit rate:7.1s/it remaining:1:55:17 elapsed:1:05:13 postfix:- (1084:tqdm_logging.py:145)
12:03:12.811 [I] Progress on: 522it/1.50kit rate:7.1s/it remaining:1:55:09 elapsed:1:05:28 postfix:- (1084:tqdm_logging.py:145)
12:03:26.941 [I] Progress on: 524it/1.50kit rate:7.1s/it remaining:1:54:54 elapsed:1:05:42 postfix:- (1084:tqdm_logging.py:145)
12:03:41.040 [I] Progress on: 526it/1.50kit rate:7.1s/it remaining:1:54:31 elapsed:1:05:56 postfix:- (1084:tqdm_logging.py:145)
12:03:55.141 [I] Progress on: 528it/1.50kit rate:7.1s/it remaining:1:54:15 elapsed:1:06:10 postfix:- (1084:tqdm_logging.py:145)
12:04:09.285 [I] Progress on: 530it/1.50kit rate:7.1s/it remaining:1:54:12 elapsed:1:06:24 postfix:- (1084:tqdm_logging.py:145)
12:04:23.418 [I] Progress on: 532it/1.50kit rate:7.1s/it remaining:1:53:59 elapsed:1:06:38 postfix:- (1084:tqdm_logging.py:145)
12:04:37.507 [I] Progress on: 534it/1.50kit rate:7.1s/it remaining:1:53:34 elapsed:1:06:52 postfix:- (1084:tqdm_logging.py:145)
12:04:52.197 [I] Progress on: 536it/1.50kit rate:7.2s/it remaining:1:56:08 elapsed:1:07:07 postfix:- (1084:tqdm_logging.py:145)
12:05:05.766 [I] Progress on: 538it/1.50kit rate:7.0s/it remaining:1:52:43 elapsed:1:07:20 postfix:- (1084:tqdm_logging.py:145)
12:05:19.880 [I] Progress on: 540it/1.50kit rate:7.0s/it remaining:1:52:46 elapsed:1:07:35 postfix:- (1084:tqdm_logging.py:145)
12:05:33.961 [I] Progress on: 542it/1.50kit rate:7.0s/it remaining:1:52:31 elapsed:1:07:49 postfix:- (1084:tqdm_logging.py:145)
12:05:48.082 [I] Progress on: 544it/1.50kit rate:7.1s/it remaining:1:52:21 elapsed:1:08:03 postfix:- (1084:tqdm_logging.py:145)
12:06:02.191 [I] Progress on: 546it/1.50kit rate:7.1s/it remaining:1:52:09 elapsed:1:08:17 postfix:- (1084:tqdm_logging.py:145)
12:06:16.306 [I] Progress on: 548it/1.50kit rate:7.1s/it remaining:1:51:57 elapsed:1:08:31 postfix:- (1084:tqdm_logging.py:145)
12:06:30.430 [I] Progress on: 550it/1.50kit rate:7.1s/it remaining:1:51:47 elapsed:1:08:45 postfix:- (1084:tqdm_logging.py:145)


Step 550: grad_norm=0.0493, loss=0.0043, param_norm=1804.2368
12:06:44.180 [I] Progress on: 550it/1.50kit rate:7.1s/it remaining:1:51:47 elapsed:1:08:59 postfix:- (1084:tqdm_logging.py:145)
12:06:58.746 [I] Progress on: 554it/1.50kit rate:6.8s/it remaining:1:46:49 elapsed:1:09:13 postfix:- (1084:tqdm_logging.py:145)
12:07:12.855 [I] Progress on: 556it/1.50kit rate:6.9s/it remaining:1:48:51 elapsed:1:09:28 postfix:- (1084:tqdm_logging.py:145)
12:07:26.968 [I] Progress on: 558it/1.50kit rate:7.0s/it remaining:1:49:44 elapsed:1:09:42 postfix:- (1084:tqdm_logging.py:145)
12:07:41.080 [I] Progress on: 560it/1.50kit rate:7.0s/it remaining:1:50:03 elapsed:1:09:56 postfix:- (1084:tqdm_logging.py:145)
12:07:55.196 [I] Progress on: 562it/1.50kit rate:7.0s/it remaining:1:50:05 elapsed:1:10:10 postfix:- (1084:tqdm_logging.py:145)
12:08:09.311 [I] Progress on: 564it/1.50kit rate:7.0s/it remaining:1:49:57 elapsed:1:10:24 postfix:- (1084:tqdm_logging.py:145)
12:08:23.430 [I] Progress on: 566it/1.50kit rate:7.1s/it remaining:1:49:46 elapsed:1:10:38 postfix:- (1084:tqdm_logging.py:145)
12:08:37.545 [I] Progress on: 568it/1.50kit rate:7.1s/it remaining:1:49:34 elapsed:1:10:52 postfix:- (1084:tqdm_logging.py:145)
12:08:51.665 [I] Progress on: 570it/1.50kit rate:7.1s/it remaining:1:49:22 elapsed:1:11:06 postfix:- (1084:tqdm_logging.py:145)
12:09:05.772 [I] Progress on: 572it/1.50kit rate:7.1s/it remaining:1:49:07 elapsed:1:11:20 postfix:- (1084:tqdm_logging.py:145)
12:09:19.892 [I] Progress on: 574it/1.50kit rate:7.1s/it remaining:1:48:55 elapsed:1:11:35 postfix:- (1084:tqdm_logging.py:145)
12:09:34.005 [I] Progress on: 576it/1.50kit rate:7.1s/it remaining:1:48:41 elapsed:1:11:49 postfix:- (1084:tqdm_logging.py:145)
12:09:48.127 [I] Progress on: 578it/1.50kit rate:7.1s/it remaining:1:48:28 elapsed:1:12:03 postfix:- (1084:tqdm_logging.py:145)
12:10:02.244 [I] Progress on: 580it/1.50kit rate:7.1s/it remaining:1:48:14 elapsed:1:12:17 postfix:- (1084:tqdm_logging.py:145)
12:10:16.359 [I] Progress on: 582it/1.50kit rate:7.1s/it remaining:1:48:00 elapsed:1:12:31 postfix:- (1084:tqdm_logging.py:145)
12:10:30.471 [I] Progress on: 584it/1.50kit rate:7.1s/it remaining:1:47:45 elapsed:1:12:45 postfix:- (1084:tqdm_logging.py:145)
12:10:44.587 [I] Progress on: 586it/1.50kit rate:7.1s/it remaining:1:47:31 elapsed:1:12:59 postfix:- (1084:tqdm_logging.py:145)
12:10:58.707 [I] Progress on: 588it/1.50kit rate:7.1s/it remaining:1:47:18 elapsed:1:13:13 postfix:- (1084:tqdm_logging.py:145)
12:11:12.823 [I] Progress on: 590it/1.50kit rate:7.1s/it remaining:1:47:04 elapsed:1:13:28 postfix:- (1084:tqdm_logging.py:145)
12:11:26.944 [I] Progress on: 592it/1.50kit rate:7.1s/it remaining:1:46:51 elapsed:1:13:42 postfix:- (1084:tqdm_logging.py:145)
12:11:41.068 [I] Progress on: 594it/1.50kit rate:7.1s/it remaining:1:46:38 elapsed:1:13:56 postfix:- (1084:tqdm_logging.py:145)
12:11:55.159 [I] Progress on: 596it/1.50kit rate:7.1s/it remaining:1:46:16 elapsed:1:14:10 postfix:- (1084:tqdm_logging.py:145)
12:12:09.277 [I] Progress on: 598it/1.50kit rate:7.1s/it remaining:1:46:04 elapsed:1:14:24 postfix:- (1084:tqdm_logging.py:145)
12:12:23.399 [I] Progress on: 600it/1.50kit rate:7.1s/it remaining:1:45:53 elapsed:1:14:38 postfix:- (1084:tqdm_logging.py:145)


Step 600: grad_norm=0.0451, loss=0.0040, param_norm=1804.2385
12:12:37.180 [I] Progress on: 600it/1.50kit rate:7.1s/it remaining:1:45:53 elapsed:1:14:52 postfix:- (1084:tqdm_logging.py:145)
12:12:51.765 [I] Progress on: 604it/1.50kit rate:6.8s/it remaining:1:41:11 elapsed:1:15:06 postfix:- (1084:tqdm_logging.py:145)
12:13:05.827 [I] Progress on: 606it/1.50kit rate:6.9s/it remaining:1:42:53 elapsed:1:15:21 postfix:- (1084:tqdm_logging.py:145)
12:13:19.927 [I] Progress on: 608it/1.50kit rate:7.0s/it remaining:1:43:45 elapsed:1:15:35 postfix:- (1084:tqdm_logging.py:145)
12:13:34.040 [I] Progress on: 610it/1.50kit rate:7.0s/it remaining:1:44:05 elapsed:1:15:49 postfix:- (1084:tqdm_logging.py:145)
12:13:48.143 [I] Progress on: 612it/1.50kit rate:7.0s/it remaining:1:44:05 elapsed:1:16:03 postfix:- (1084:tqdm_logging.py:145)
12:14:02.261 [I] Progress on: 614it/1.50kit rate:7.0s/it remaining:1:44:03 elapsed:1:16:17 postfix:- (1084:tqdm_logging.py:145)
12:14:16.379 [I] Progress on: 616it/1.50kit rate:7.1s/it remaining:1:43:55 elapsed:1:16:31 postfix:- (1084:tqdm_logging.py:145)
12:14:30.481 [I] Progress on: 618it/1.50kit rate:7.1s/it remaining:1:43:40 elapsed:1:16:45 postfix:- (1084:tqdm_logging.py:145)
12:14:44.616 [I] Progress on: 620it/1.50kit rate:7.1s/it remaining:1:43:32 elapsed:1:16:59 postfix:- (1084:tqdm_logging.py:145)
12:14:58.719 [I] Progress on: 622it/1.50kit rate:7.1s/it remaining:1:43:15 elapsed:1:17:13 postfix:- (1084:tqdm_logging.py:145)
12:15:13.232 [I] Progress on: 624it/1.50kit rate:7.1s/it remaining:1:44:16 elapsed:1:17:28 postfix:- (1084:tqdm_logging.py:145)
12:15:27.331 [I] Progress on: 626it/1.50kit rate:7.1s/it remaining:1:43:22 elapsed:1:17:42 postfix:- (1084:tqdm_logging.py:145)
12:15:41.450 [I] Progress on: 628it/1.50kit rate:7.1s/it remaining:1:42:51 elapsed:1:17:56 postfix:- (1084:tqdm_logging.py:145)
12:15:55.537 [I] Progress on: 630it/1.50kit rate:7.1s/it remaining:1:42:21 elapsed:1:18:10 postfix:- (1084:tqdm_logging.py:145)
12:16:09.638 [I] Progress on: 632it/1.50kit rate:7.1s/it remaining:1:42:01 elapsed:1:18:24 postfix:- (1084:tqdm_logging.py:145)
12:16:23.733 [I] Progress on: 634it/1.50kit rate:7.0s/it remaining:1:41:44 elapsed:1:18:38 postfix:- (1084:tqdm_logging.py:145)
12:16:37.846 [I] Progress on: 636it/1.50kit rate:7.1s/it remaining:1:41:32 elapsed:1:18:53 postfix:- (1084:tqdm_logging.py:145)
12:16:51.950 [I] Progress on: 638it/1.50kit rate:7.1s/it remaining:1:41:19 elapsed:1:19:07 postfix:- (1084:tqdm_logging.py:145)
12:17:06.064 [I] Progress on: 640it/1.50kit rate:7.1s/it remaining:1:41:07 elapsed:1:19:21 postfix:- (1084:tqdm_logging.py:145)
12:17:20.150 [I] Progress on: 642it/1.50kit rate:7.0s/it remaining:1:40:47 elapsed:1:19:35 postfix:- (1084:tqdm_logging.py:145)
12:17:34.259 [I] Progress on: 644it/1.50kit rate:7.1s/it remaining:1:40:37 elapsed:1:19:49 postfix:- (1084:tqdm_logging.py:145)
12:17:48.367 [I] Progress on: 646it/1.50kit rate:7.1s/it remaining:1:40:23 elapsed:1:20:03 postfix:- (1084:tqdm_logging.py:145)
12:18:02.484 [I] Progress on: 648it/1.50kit rate:7.1s/it remaining:1:40:11 elapsed:1:20:17 postfix:- (1084:tqdm_logging.py:145)
12:18:16.602 [I] Progress on: 650it/1.50kit rate:7.1s/it remaining:1:39:59 elapsed:1:20:31 postfix:- (1084:tqdm_logging.py:145)


Step 650: grad_norm=0.0486, loss=0.0039, param_norm=1804.2402
12:18:30.363 [I] Progress on: 650it/1.50kit rate:7.1s/it remaining:1:39:59 elapsed:1:20:45 postfix:- (1084:tqdm_logging.py:145)
12:18:44.901 [I] Progress on: 654it/1.50kit rate:6.8s/it remaining:1:35:26 elapsed:1:21:00 postfix:- (1084:tqdm_logging.py:145)
12:18:59.010 [I] Progress on: 656it/1.50kit rate:6.9s/it remaining:1:37:15 elapsed:1:21:14 postfix:- (1084:tqdm_logging.py:145)
12:19:13.132 [I] Progress on: 658it/1.50kit rate:7.0s/it remaining:1:38:05 elapsed:1:21:28 postfix:- (1084:tqdm_logging.py:145)
12:19:27.245 [I] Progress on: 660it/1.50kit rate:7.0s/it remaining:1:38:20 elapsed:1:21:42 postfix:- (1084:tqdm_logging.py:145)
12:19:41.353 [I] Progress on: 662it/1.50kit rate:7.0s/it remaining:1:38:19 elapsed:1:21:56 postfix:- (1084:tqdm_logging.py:145)
12:19:55.472 [I] Progress on: 664it/1.50kit rate:7.1s/it remaining:1:38:14 elapsed:1:22:10 postfix:- (1084:tqdm_logging.py:145)
12:20:09.590 [I] Progress on: 666it/1.50kit rate:7.1s/it remaining:1:38:03 elapsed:1:22:24 postfix:- (1084:tqdm_logging.py:145)
12:20:23.698 [I] Progress on: 668it/1.50kit rate:7.1s/it remaining:1:37:49 elapsed:1:22:38 postfix:- (1084:tqdm_logging.py:145)
12:20:38.290 [I] Progress on: 670it/1.50kit rate:7.2s/it remaining:1:39:36 elapsed:1:22:53 postfix:- (1084:tqdm_logging.py:145)
12:20:51.917 [I] Progress on: 672it/1.50kit rate:7.0s/it remaining:1:36:57 elapsed:1:23:07 postfix:- (1084:tqdm_logging.py:145)
12:21:06.021 [I] Progress on: 674it/1.50kit rate:7.0s/it remaining:1:36:53 elapsed:1:23:21 postfix:- (1084:tqdm_logging.py:145)
12:21:20.131 [I] Progress on: 676it/1.50kit rate:7.0s/it remaining:1:36:46 elapsed:1:23:35 postfix:- (1084:tqdm_logging.py:145)
12:21:34.245 [I] Progress on: 678it/1.50kit rate:7.1s/it remaining:1:36:37 elapsed:1:23:49 postfix:- (1084:tqdm_logging.py:145)
12:21:48.355 [I] Progress on: 680it/1.50kit rate:7.1s/it remaining:1:36:24 elapsed:1:24:03 postfix:- (1084:tqdm_logging.py:145)
12:22:02.432 [I] Progress on: 682it/1.50kit rate:7.0s/it remaining:1:36:03 elapsed:1:24:17 postfix:- (1084:tqdm_logging.py:145)
12:22:16.545 [I] Progress on: 684it/1.50kit rate:7.1s/it remaining:1:35:53 elapsed:1:24:31 postfix:- (1084:tqdm_logging.py:145)
12:22:30.666 [I] Progress on: 686it/1.50kit rate:7.1s/it remaining:1:35:43 elapsed:1:24:45 postfix:- (1084:tqdm_logging.py:145)
12:22:44.782 [I] Progress on: 688it/1.50kit rate:7.1s/it remaining:1:35:29 elapsed:1:24:59 postfix:- (1084:tqdm_logging.py:145)
12:22:58.896 [I] Progress on: 690it/1.50kit rate:7.1s/it remaining:1:35:16 elapsed:1:25:14 postfix:- (1084:tqdm_logging.py:145)
12:23:13.002 [I] Progress on: 692it/1.50kit rate:7.1s/it remaining:1:35:01 elapsed:1:25:28 postfix:- (1084:tqdm_logging.py:145)
12:23:27.147 [I] Progress on: 694it/1.50kit rate:7.1s/it remaining:1:34:47 elapsed:1:25:42 postfix:- (1084:tqdm_logging.py:145)
12:23:41.285 [I] Progress on: 696it/1.50kit rate:7.1s/it remaining:1:34:40 elapsed:1:25:56 postfix:- (1084:tqdm_logging.py:145)
12:23:55.372 [I] Progress on: 698it/1.50kit rate:7.1s/it remaining:1:34:17 elapsed:1:26:10 postfix:- (1084:tqdm_logging.py:145)
12:24:09.470 [I] Progress on: 700it/1.50kit rate:7.1s/it remaining:1:34:01 elapsed:1:26:24 postfix:- (1084:tqdm_logging.py:145)


Step 700: grad_norm=0.0511, loss=0.0039, param_norm=1804.2418
12:24:23.237 [I] Progress on: 700it/1.50kit rate:7.1s/it remaining:1:34:01 elapsed:1:26:38 postfix:- (1084:tqdm_logging.py:145)
12:24:37.807 [I] Progress on: 704it/1.50kit rate:6.8s/it remaining:1:29:51 elapsed:1:26:52 postfix:- (1084:tqdm_logging.py:145)
12:24:51.952 [I] Progress on: 706it/1.50kit rate:6.9s/it remaining:1:31:40 elapsed:1:27:07 postfix:- (1084:tqdm_logging.py:145)
12:25:06.039 [I] Progress on: 708it/1.50kit rate:7.0s/it remaining:1:32:14 elapsed:1:27:21 postfix:- (1084:tqdm_logging.py:145)
12:25:20.161 [I] Progress on: 710it/1.50kit rate:7.0s/it remaining:1:32:30 elapsed:1:27:35 postfix:- (1084:tqdm_logging.py:145)
12:25:34.263 [I] Progress on: 712it/1.50kit rate:7.0s/it remaining:1:32:26 elapsed:1:27:49 postfix:- (1084:tqdm_logging.py:145)
12:25:48.373 [I] Progress on: 714it/1.50kit rate:7.0s/it remaining:1:32:18 elapsed:1:28:03 postfix:- (1084:tqdm_logging.py:145)
12:26:02.503 [I] Progress on: 716it/1.50kit rate:7.1s/it remaining:1:32:14 elapsed:1:28:17 postfix:- (1084:tqdm_logging.py:145)
12:26:16.598 [I] Progress on: 718it/1.50kit rate:7.1s/it remaining:1:31:55 elapsed:1:28:31 postfix:- (1084:tqdm_logging.py:145)
12:26:30.705 [I] Progress on: 720it/1.50kit rate:7.1s/it remaining:1:31:42 elapsed:1:28:45 postfix:- (1084:tqdm_logging.py:145)
12:26:44.821 [I] Progress on: 722it/1.50kit rate:7.1s/it remaining:1:31:29 elapsed:1:29:00 postfix:- (1084:tqdm_logging.py:145)
12:26:58.945 [I] Progress on: 724it/1.50kit rate:7.1s/it remaining:1:31:16 elapsed:1:29:14 postfix:- (1084:tqdm_logging.py:145)
12:27:13.058 [I] Progress on: 726it/1.50kit rate:7.1s/it remaining:1:31:01 elapsed:1:29:28 postfix:- (1084:tqdm_logging.py:145)
12:27:27.172 [I] Progress on: 728it/1.50kit rate:7.1s/it remaining:1:30:46 elapsed:1:29:42 postfix:- (1084:tqdm_logging.py:145)
12:27:41.268 [I] Progress on: 730it/1.50kit rate:7.1s/it remaining:1:30:30 elapsed:1:29:56 postfix:- (1084:tqdm_logging.py:145)
12:27:55.390 [I] Progress on: 732it/1.50kit rate:7.1s/it remaining:1:30:20 elapsed:1:30:10 postfix:- (1084:tqdm_logging.py:145)
12:28:09.497 [I] Progress on: 734it/1.50kit rate:7.1s/it remaining:1:30:05 elapsed:1:30:24 postfix:- (1084:tqdm_logging.py:145)
12:28:23.597 [I] Progress on: 736it/1.50kit rate:7.1s/it remaining:1:29:49 elapsed:1:30:38 postfix:- (1084:tqdm_logging.py:145)
12:28:37.722 [I] Progress on: 738it/1.50kit rate:7.1s/it remaining:1:29:39 elapsed:1:30:52 postfix:- (1084:tqdm_logging.py:145)
12:28:51.831 [I] Progress on: 740it/1.50kit rate:7.1s/it remaining:1:29:23 elapsed:1:31:07 postfix:- (1084:tqdm_logging.py:145)
12:29:05.936 [I] Progress on: 742it/1.50kit rate:7.1s/it remaining:1:29:08 elapsed:1:31:21 postfix:- (1084:tqdm_logging.py:145)
12:29:20.020 [I] Progress on: 744it/1.50kit rate:7.0s/it remaining:1:28:48 elapsed:1:31:35 postfix:- (1084:tqdm_logging.py:145)
12:29:34.138 [I] Progress on: 746it/1.50kit rate:7.1s/it remaining:1:28:38 elapsed:1:31:49 postfix:- (1084:tqdm_logging.py:145)
12:29:48.279 [I] Progress on: 748it/1.50kit rate:7.1s/it remaining:1:28:31 elapsed:1:32:03 postfix:- (1084:tqdm_logging.py:145)
12:30:02.386 [I] Progress on: 750it/1.50kit rate:7.1s/it remaining:1:28:14 elapsed:1:32:17 postfix:- (1084:tqdm_logging.py:145)


Step 750: grad_norm=0.0501, loss=0.0036, param_norm=1804.2430
12:30:16.178 [I] Progress on: 750it/1.50kit rate:7.1s/it remaining:1:28:14 elapsed:1:32:31 postfix:- (1084:tqdm_logging.py:145)
12:30:30.753 [I] Progress on: 754it/1.50kit rate:6.8s/it remaining:1:24:14 elapsed:1:32:45 postfix:- (1084:tqdm_logging.py:145)
12:30:44.839 [I] Progress on: 756it/1.50kit rate:6.9s/it remaining:1:25:42 elapsed:1:33:00 postfix:- (1084:tqdm_logging.py:145)
12:30:58.959 [I] Progress on: 758it/1.50kit rate:7.0s/it remaining:1:26:24 elapsed:1:33:14 postfix:- (1084:tqdm_logging.py:145)
12:31:13.074 [I] Progress on: 760it/1.50kit rate:7.0s/it remaining:1:26:36 elapsed:1:33:28 postfix:- (1084:tqdm_logging.py:145)
12:31:27.169 [I] Progress on: 762it/1.50kit rate:7.0s/it remaining:1:26:31 elapsed:1:33:42 postfix:- (1084:tqdm_logging.py:145)
12:31:41.277 [I] Progress on: 764it/1.50kit rate:7.0s/it remaining:1:26:24 elapsed:1:33:56 postfix:- (1084:tqdm_logging.py:145)
12:31:55.374 [I] Progress on: 766it/1.50kit rate:7.0s/it remaining:1:26:11 elapsed:1:34:10 postfix:- (1084:tqdm_logging.py:145)
12:32:09.489 [I] Progress on: 768it/1.50kit rate:7.1s/it remaining:1:26:01 elapsed:1:34:24 postfix:- (1084:tqdm_logging.py:145)
12:32:23.612 [I] Progress on: 770it/1.50kit rate:7.1s/it remaining:1:25:51 elapsed:1:34:38 postfix:- (1084:tqdm_logging.py:145)
12:32:37.714 [I] Progress on: 772it/1.50kit rate:7.1s/it remaining:1:25:36 elapsed:1:34:52 postfix:- (1084:tqdm_logging.py:145)
12:32:51.817 [I] Progress on: 774it/1.50kit rate:7.1s/it remaining:1:25:22 elapsed:1:35:07 postfix:- (1084:tqdm_logging.py:145)
12:33:05.927 [I] Progress on: 776it/1.50kit rate:7.1s/it remaining:1:25:08 elapsed:1:35:21 postfix:- (1084:tqdm_logging.py:145)
12:33:20.040 [I] Progress on: 778it/1.50kit rate:7.1s/it remaining:1:24:54 elapsed:1:35:35 postfix:- (1084:tqdm_logging.py:145)
12:33:34.139 [I] Progress on: 780it/1.50kit rate:7.1s/it remaining:1:24:37 elapsed:1:35:49 postfix:- (1084:tqdm_logging.py:145)
12:33:48.268 [I] Progress on: 782it/1.50kit rate:7.1s/it remaining:1:24:27 elapsed:1:36:03 postfix:- (1084:tqdm_logging.py:145)
12:34:02.383 [I] Progress on: 784it/1.50kit rate:7.1s/it remaining:1:24:13 elapsed:1:36:17 postfix:- (1084:tqdm_logging.py:145)
12:34:16.481 [I] Progress on: 786it/1.50kit rate:7.1s/it remaining:1:23:56 elapsed:1:36:31 postfix:- (1084:tqdm_logging.py:145)
12:34:30.587 [I] Progress on: 788it/1.50kit rate:7.1s/it remaining:1:23:42 elapsed:1:36:45 postfix:- (1084:tqdm_logging.py:145)
12:34:44.716 [I] Progress on: 790it/1.50kit rate:7.1s/it remaining:1:23:32 elapsed:1:36:59 postfix:- (1084:tqdm_logging.py:145)
12:34:58.811 [I] Progress on: 792it/1.50kit rate:7.1s/it remaining:1:23:14 elapsed:1:37:14 postfix:- (1084:tqdm_logging.py:145)
12:35:12.942 [I] Progress on: 794it/1.50kit rate:7.1s/it remaining:1:23:04 elapsed:1:37:28 postfix:- (1084:tqdm_logging.py:145)
12:35:27.039 [I] Progress on: 796it/1.50kit rate:7.1s/it remaining:1:22:46 elapsed:1:37:42 postfix:- (1084:tqdm_logging.py:145)
12:35:41.151 [I] Progress on: 798it/1.50kit rate:7.1s/it remaining:1:22:32 elapsed:1:37:56 postfix:- (1084:tqdm_logging.py:145)
12:35:55.282 [I] Progress on: 800it/1.50kit rate:7.1s/it remaining:1:22:22 elapsed:1:38:10 postfix:- (1084:tqdm_logging.py:145)


Step 800: grad_norm=0.0439, loss=0.0034, param_norm=1804.2444
12:36:09.001 [I] Progress on: 800it/1.50kit rate:7.1s/it remaining:1:22:22 elapsed:1:38:24 postfix:- (1084:tqdm_logging.py:145)
12:36:24.084 [I] Progress on: 804it/1.50kit rate:6.9s/it remaining:1:20:24 elapsed:1:38:39 postfix:- (1084:tqdm_logging.py:145)
12:36:37.632 [I] Progress on: 806it/1.50kit rate:6.9s/it remaining:1:19:31 elapsed:1:38:52 postfix:- (1084:tqdm_logging.py:145)
12:36:51.762 [I] Progress on: 808it/1.50kit rate:7.0s/it remaining:1:20:24 elapsed:1:39:06 postfix:- (1084:tqdm_logging.py:145)
12:37:05.870 [I] Progress on: 810it/1.50kit rate:7.0s/it remaining:1:20:39 elapsed:1:39:21 postfix:- (1084:tqdm_logging.py:145)
12:37:19.979 [I] Progress on: 812it/1.50kit rate:7.0s/it remaining:1:20:40 elapsed:1:39:35 postfix:- (1084:tqdm_logging.py:145)
12:37:34.104 [I] Progress on: 814it/1.50kit rate:7.0s/it remaining:1:20:35 elapsed:1:39:49 postfix:- (1084:tqdm_logging.py:145)
12:37:48.219 [I] Progress on: 816it/1.50kit rate:7.1s/it remaining:1:20:24 elapsed:1:40:03 postfix:- (1084:tqdm_logging.py:145)
12:38:02.334 [I] Progress on: 818it/1.50kit rate:7.1s/it remaining:1:20:12 elapsed:1:40:17 postfix:- (1084:tqdm_logging.py:145)
12:38:16.453 [I] Progress on: 820it/1.50kit rate:7.1s/it remaining:1:20:00 elapsed:1:40:31 postfix:- (1084:tqdm_logging.py:145)
12:38:30.544 [I] Progress on: 822it/1.50kit rate:7.1s/it remaining:1:19:41 elapsed:1:40:45 postfix:- (1084:tqdm_logging.py:145)
12:38:44.659 [I] Progress on: 824it/1.50kit rate:7.1s/it remaining:1:19:28 elapsed:1:40:59 postfix:- (1084:tqdm_logging.py:145)
12:38:58.768 [I] Progress on: 826it/1.50kit rate:7.1s/it remaining:1:19:14 elapsed:1:41:13 postfix:- (1084:tqdm_logging.py:145)
12:39:12.875 [I] Progress on: 828it/1.50kit rate:7.1s/it remaining:1:18:59 elapsed:1:41:28 postfix:- (1084:tqdm_logging.py:145)
12:39:27.003 [I] Progress on: 830it/1.50kit rate:7.1s/it remaining:1:18:49 elapsed:1:41:42 postfix:- (1084:tqdm_logging.py:145)
12:39:41.122 [I] Progress on: 832it/1.50kit rate:7.1s/it remaining:1:18:32 elapsed:1:41:56 postfix:- (1084:tqdm_logging.py:145)
12:39:55.237 [I] Progress on: 834it/1.50kit rate:7.1s/it remaining:1:18:21 elapsed:1:42:10 postfix:- (1084:tqdm_logging.py:145)
12:40:09.330 [I] Progress on: 836it/1.50kit rate:7.1s/it remaining:1:18:04 elapsed:1:42:24 postfix:- (1084:tqdm_logging.py:145)
12:40:23.427 [I] Progress on: 838it/1.50kit rate:7.1s/it remaining:1:17:47 elapsed:1:42:38 postfix:- (1084:tqdm_logging.py:145)
12:40:37.528 [I] Progress on: 840it/1.50kit rate:7.1s/it remaining:1:17:33 elapsed:1:42:52 postfix:- (1084:tqdm_logging.py:145)
12:40:51.615 [I] Progress on: 842it/1.50kit rate:7.0s/it remaining:1:17:17 elapsed:1:43:06 postfix:- (1084:tqdm_logging.py:145)
12:41:05.735 [I] Progress on: 844it/1.50kit rate:7.1s/it remaining:1:17:07 elapsed:1:43:20 postfix:- (1084:tqdm_logging.py:145)
12:41:19.839 [I] Progress on: 846it/1.50kit rate:7.1s/it remaining:1:16:53 elapsed:1:43:35 postfix:- (1084:tqdm_logging.py:145)
12:41:33.948 [I] Progress on: 848it/1.50kit rate:7.1s/it remaining:1:16:39 elapsed:1:43:49 postfix:- (1084:tqdm_logging.py:145)
12:41:48.045 [I] Progress on: 850it/1.50kit rate:7.1s/it remaining:1:16:23 elapsed:1:44:03 postfix:- (1084:tqdm_logging.py:145)


Step 850: grad_norm=0.0455, loss=0.0033, param_norm=1804.2455
12:42:01.820 [I] Progress on: 850it/1.50kit rate:7.1s/it remaining:1:16:23 elapsed:1:44:17 postfix:- (1084:tqdm_logging.py:145)
12:42:16.367 [I] Progress on: 854it/1.50kit rate:6.8s/it remaining:1:12:55 elapsed:1:44:31 postfix:- (1084:tqdm_logging.py:145)
12:42:30.470 [I] Progress on: 856it/1.50kit rate:6.9s/it remaining:1:14:13 elapsed:1:44:45 postfix:- (1084:tqdm_logging.py:145)
12:42:44.582 [I] Progress on: 858it/1.50kit rate:7.0s/it remaining:1:14:46 elapsed:1:44:59 postfix:- (1084:tqdm_logging.py:145)
12:42:58.675 [I] Progress on: 860it/1.50kit rate:7.0s/it remaining:1:14:51 elapsed:1:45:13 postfix:- (1084:tqdm_logging.py:145)
12:43:12.818 [I] Progress on: 862it/1.50kit rate:7.0s/it remaining:1:14:55 elapsed:1:45:28 postfix:- (1084:tqdm_logging.py:145)
12:43:26.912 [I] Progress on: 864it/1.50kit rate:7.0s/it remaining:1:14:42 elapsed:1:45:42 postfix:- (1084:tqdm_logging.py:145)
12:43:41.004 [I] Progress on: 866it/1.50kit rate:7.0s/it remaining:1:14:28 elapsed:1:45:56 postfix:- (1084:tqdm_logging.py:145)
12:43:55.104 [I] Progress on: 868it/1.50kit rate:7.0s/it remaining:1:14:15 elapsed:1:46:10 postfix:- (1084:tqdm_logging.py:145)
12:44:09.243 [I] Progress on: 870it/1.50kit rate:7.1s/it remaining:1:14:07 elapsed:1:46:24 postfix:- (1084:tqdm_logging.py:145)
12:44:23.377 [I] Progress on: 872it/1.50kit rate:7.1s/it remaining:1:13:56 elapsed:1:46:38 postfix:- (1084:tqdm_logging.py:145)
12:44:37.456 [I] Progress on: 874it/1.50kit rate:7.1s/it remaining:1:13:35 elapsed:1:46:52 postfix:- (1084:tqdm_logging.py:145)
12:44:51.561 [I] Progress on: 876it/1.50kit rate:7.1s/it remaining:1:13:21 elapsed:1:47:06 postfix:- (1084:tqdm_logging.py:145)
12:45:05.664 [I] Progress on: 878it/1.50kit rate:7.1s/it remaining:1:13:08 elapsed:1:47:20 postfix:- (1084:tqdm_logging.py:145)
12:45:19.771 [I] Progress on: 880it/1.50kit rate:7.1s/it remaining:1:12:53 elapsed:1:47:34 postfix:- (1084:tqdm_logging.py:145)
12:45:33.908 [I] Progress on: 882it/1.50kit rate:7.1s/it remaining:1:12:44 elapsed:1:47:49 postfix:- (1084:tqdm_logging.py:145)
12:45:48.011 [I] Progress on: 884it/1.50kit rate:7.1s/it remaining:1:12:28 elapsed:1:48:03 postfix:- (1084:tqdm_logging.py:145)
12:46:02.075 [I] Progress on: 886it/1.50kit rate:7.0s/it remaining:1:12:05 elapsed:1:48:17 postfix:- (1084:tqdm_logging.py:145)
12:46:16.208 [I] Progress on: 888it/1.50kit rate:7.1s/it remaining:1:11:58 elapsed:1:48:31 postfix:- (1084:tqdm_logging.py:145)
12:46:30.309 [I] Progress on: 890it/1.50kit rate:7.1s/it remaining:1:11:42 elapsed:1:48:45 postfix:- (1084:tqdm_logging.py:145)
12:46:44.449 [I] Progress on: 892it/1.50kit rate:7.1s/it remaining:1:11:33 elapsed:1:48:59 postfix:- (1084:tqdm_logging.py:145)
12:46:58.526 [I] Progress on: 894it/1.50kit rate:7.1s/it remaining:1:11:12 elapsed:1:49:13 postfix:- (1084:tqdm_logging.py:145)
12:47:12.636 [I] Progress on: 896it/1.50kit rate:7.1s/it remaining:1:10:59 elapsed:1:49:27 postfix:- (1084:tqdm_logging.py:145)
12:47:26.747 [I] Progress on: 898it/1.50kit rate:7.1s/it remaining:1:10:45 elapsed:1:49:41 postfix:- (1084:tqdm_logging.py:145)
12:47:40.848 [I] Progress on: 900it/1.50kit rate:7.1s/it remaining:1:10:30 elapsed:1:49:56 postfix:- (1084:tqdm_logging.py:145)


Step 900: grad_norm=0.0463, loss=0.0035, param_norm=1804.2463
12:47:54.648 [I] Progress on: 900it/1.50kit rate:7.1s/it remaining:1:10:30 elapsed:1:50:09 postfix:- (1084:tqdm_logging.py:145)
12:48:09.250 [I] Progress on: 904it/1.50kit rate:6.8s/it remaining:1:07:20 elapsed:1:50:24 postfix:- (1084:tqdm_logging.py:145)
12:48:23.341 [I] Progress on: 906it/1.50kit rate:6.9s/it remaining:1:08:27 elapsed:1:50:38 postfix:- (1084:tqdm_logging.py:145)
12:48:37.474 [I] Progress on: 908it/1.50kit rate:7.0s/it remaining:1:08:57 elapsed:1:50:52 postfix:- (1084:tqdm_logging.py:145)
12:48:51.558 [I] Progress on: 910it/1.50kit rate:7.0s/it remaining:1:08:59 elapsed:1:51:06 postfix:- (1084:tqdm_logging.py:145)
12:49:05.665 [I] Progress on: 912it/1.50kit rate:7.0s/it remaining:1:08:56 elapsed:1:51:20 postfix:- (1084:tqdm_logging.py:145)
12:49:19.784 [I] Progress on: 914it/1.50kit rate:7.0s/it remaining:1:08:49 elapsed:1:51:34 postfix:- (1084:tqdm_logging.py:145)
12:49:33.896 [I] Progress on: 916it/1.50kit rate:7.1s/it remaining:1:08:38 elapsed:1:51:49 postfix:- (1084:tqdm_logging.py:145)
12:49:48.011 [I] Progress on: 918it/1.50kit rate:7.1s/it remaining:1:08:26 elapsed:1:52:03 postfix:- (1084:tqdm_logging.py:145)
12:50:02.103 [I] Progress on: 920it/1.50kit rate:7.1s/it remaining:1:08:10 elapsed:1:52:17 postfix:- (1084:tqdm_logging.py:145)
12:50:16.218 [I] Progress on: 922it/1.50kit rate:7.1s/it remaining:1:07:57 elapsed:1:52:31 postfix:- (1084:tqdm_logging.py:145)
12:50:30.320 [I] Progress on: 924it/1.50kit rate:7.1s/it remaining:1:07:43 elapsed:1:52:45 postfix:- (1084:tqdm_logging.py:145)
12:50:44.419 [I] Progress on: 926it/1.50kit rate:7.1s/it remaining:1:07:27 elapsed:1:52:59 postfix:- (1084:tqdm_logging.py:145)
12:50:58.509 [I] Progress on: 928it/1.50kit rate:7.0s/it remaining:1:07:10 elapsed:1:53:13 postfix:- (1084:tqdm_logging.py:145)
12:51:12.619 [I] Progress on: 930it/1.50kit rate:7.0s/it remaining:1:06:58 elapsed:1:53:27 postfix:- (1084:tqdm_logging.py:145)
12:51:26.708 [I] Progress on: 932it/1.50kit rate:7.0s/it remaining:1:06:42 elapsed:1:53:41 postfix:- (1084:tqdm_logging.py:145)
12:51:40.828 [I] Progress on: 934it/1.50kit rate:7.1s/it remaining:1:06:31 elapsed:1:53:56 postfix:- (1084:tqdm_logging.py:145)
12:51:54.937 [I] Progress on: 936it/1.50kit rate:7.1s/it remaining:1:06:17 elapsed:1:54:10 postfix:- (1084:tqdm_logging.py:145)
12:52:09.658 [I] Progress on: 938it/1.50kit rate:7.2s/it remaining:1:07:47 elapsed:1:54:24 postfix:- (1084:tqdm_logging.py:145)
12:52:23.190 [I] Progress on: 940it/1.50kit rate:7.0s/it remaining:1:05:34 elapsed:1:54:38 postfix:- (1084:tqdm_logging.py:145)
12:52:37.278 [I] Progress on: 942it/1.50kit rate:7.0s/it remaining:1:05:28 elapsed:1:54:52 postfix:- (1084:tqdm_logging.py:145)
12:52:51.396 [I] Progress on: 944it/1.50kit rate:7.1s/it remaining:1:05:22 elapsed:1:55:06 postfix:- (1084:tqdm_logging.py:145)
12:53:05.506 [I] Progress on: 946it/1.50kit rate:7.1s/it remaining:1:05:10 elapsed:1:55:20 postfix:- (1084:tqdm_logging.py:145)
12:53:19.617 [I] Progress on: 948it/1.50kit rate:7.1s/it remaining:1:04:57 elapsed:1:55:34 postfix:- (1084:tqdm_logging.py:145)
12:53:33.736 [I] Progress on: 950it/1.50kit rate:7.1s/it remaining:1:04:44 elapsed:1:55:48 postfix:- (1084:tqdm_logging.py:145)


Step 950: grad_norm=0.0417, loss=0.0033, param_norm=1804.2474
12:53:47.472 [I] Progress on: 950it/1.50kit rate:7.1s/it remaining:1:04:44 elapsed:1:56:02 postfix:- (1084:tqdm_logging.py:145)
12:54:01.971 [I] Progress on: 954it/1.50kit rate:6.8s/it remaining:1:01:33 elapsed:1:56:17 postfix:- (1084:tqdm_logging.py:145)
12:54:16.081 [I] Progress on: 956it/1.50kit rate:6.9s/it remaining:1:02:40 elapsed:1:56:31 postfix:- (1084:tqdm_logging.py:145)
12:54:30.184 [I] Progress on: 958it/1.50kit rate:7.0s/it remaining:1:03:04 elapsed:1:56:45 postfix:- (1084:tqdm_logging.py:145)
12:54:44.317 [I] Progress on: 960it/1.50kit rate:7.0s/it remaining:1:03:13 elapsed:1:56:59 postfix:- (1084:tqdm_logging.py:145)
12:54:58.443 [I] Progress on: 962it/1.50kit rate:7.0s/it remaining:1:03:08 elapsed:1:57:13 postfix:- (1084:tqdm_logging.py:145)
12:55:12.567 [I] Progress on: 964it/1.50kit rate:7.1s/it remaining:1:03:00 elapsed:1:57:27 postfix:- (1084:tqdm_logging.py:145)
12:55:26.670 [I] Progress on: 966it/1.50kit rate:7.1s/it remaining:1:02:46 elapsed:1:57:41 postfix:- (1084:tqdm_logging.py:145)
12:55:40.794 [I] Progress on: 968it/1.50kit rate:7.1s/it remaining:1:02:35 elapsed:1:57:55 postfix:- (1084:tqdm_logging.py:145)
12:55:54.887 [I] Progress on: 970it/1.50kit rate:7.1s/it remaining:1:02:18 elapsed:1:58:10 postfix:- (1084:tqdm_logging.py:145)
12:56:08.995 [I] Progress on: 972it/1.50kit rate:7.1s/it remaining:1:02:04 elapsed:1:58:24 postfix:- (1084:tqdm_logging.py:145)
12:56:23.145 [I] Progress on: 974it/1.50kit rate:7.1s/it remaining:1:01:55 elapsed:1:58:38 postfix:- (1084:tqdm_logging.py:145)
12:56:37.211 [I] Progress on: 976it/1.50kit rate:7.1s/it remaining:1:01:34 elapsed:1:58:52 postfix:- (1084:tqdm_logging.py:145)
12:56:51.306 [I] Progress on: 978it/1.50kit rate:7.0s/it remaining:1:01:19 elapsed:1:59:06 postfix:- (1084:tqdm_logging.py:145)
12:57:05.420 [I] Progress on: 980it/1.50kit rate:7.1s/it remaining:1:01:07 elapsed:1:59:20 postfix:- (1084:tqdm_logging.py:145)
12:57:19.518 [I] Progress on: 982it/1.50kit rate:7.1s/it remaining:1:00:53 elapsed:1:59:34 postfix:- (1084:tqdm_logging.py:145)
12:57:33.629 [I] Progress on: 984it/1.50kit rate:7.1s/it remaining:1:00:39 elapsed:1:59:48 postfix:- (1084:tqdm_logging.py:145)
12:57:47.713 [I] Progress on: 986it/1.50kit rate:7.0s/it remaining:1:00:22 elapsed:2:00:02 postfix:- (1084:tqdm_logging.py:145)
12:58:01.829 [I] Progress on: 988it/1.50kit rate:7.1s/it remaining:1:00:11 elapsed:2:00:17 postfix:- (1084:tqdm_logging.py:145)
12:58:15.976 [I] Progress on: 990it/1.50kit rate:7.1s/it remaining:1:00:02 elapsed:2:00:31 postfix:- (1084:tqdm_logging.py:145)
12:58:30.054 [I] Progress on: 992it/1.50kit rate:7.0s/it remaining:59:41 elapsed:2:00:45 postfix:- (1084:tqdm_logging.py:145)
12:58:44.153 [I] Progress on: 994it/1.50kit rate:7.0s/it remaining:59:27 elapsed:2:00:59 postfix:- (1084:tqdm_logging.py:145)
12:58:58.273 [I] Progress on: 996it/1.50kit rate:7.1s/it remaining:59:16 elapsed:2:01:13 postfix:- (1084:tqdm_logging.py:145)
12:59:12.374 [I] Progress on: 998it/1.50kit rate:7.1s/it remaining:59:01 elapsed:2:01:27 postfix:- (1084:tqdm_logging.py:145)
12:59:26.473 [I] Progress on: 1.00kit/1.50kit rate:7.1s/it remaining:58:46 elapsed:2:01:41 postfix:- (1084:tqdm_logging.py:145)


Step 1000: grad_norm=0.0437, loss=0.0034, param_norm=1804.2480
12:59:40.264 [I] Progress on: 1.00kit/1.50kit rate:7.1s/it remaining:58:46 elapsed:2:01:55 postfix:- (1084:tqdm_logging.py:145)
12:59:40.310 [I] [process=0][thread=MainThread][wait_until_finished] No Save Finalize thread to wait for. Returning. (1084:checkpoint_manager.py:1987)
12:59:40.311 [I] [process=0] Saving checkpoint at step 1000                                       (1084:checkpoint_manager.py:1408)
12:59:40.312 [I] [process=0] Started async saving checkpoint to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000. (1084:async_checkpointer.py:439)
12:59:40.313 [I] Using ThreadSafeKeyValueSignalingClient                                          (1084:signaling_client.py:332)
12:59:40.317 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0 (1084:atomicity.py:144)
12:59:40.336 [I] Wrote Metadata={'item_handlers': None, 'metrics': {}, 'performance_metrics': {}, 'init_timestamp_nsecs': 1793969980334486312, 'commit_timestamp_nsecs': None, 'custom_metadata': {}}, json={"item_handlers": null, "metrics": {}, "performance_metrics": {}, "init_timestamp_nsecs": 1793969980334486312, "commit_timestamp_nsecs": null, "custom_metadata": {}} to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/_CHECKPOINT_METADATA (1084:checkpoint.py:186)
12:59:40.339 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/assets.orbax-checkpoint-tmp-1 (1084:atomicity.py:144)
12:59:40.339 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/params.orbax-checkpoint-tmp-2 (1084:atomicity.py:144)
12:59:40.340 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/train_state.orbax-checkpoint-tmp-3 (1084:atomicity.py:144)
12:59:40.360 [I] Transferring arrays to host memory with options: use_replica_parallel=True, enable_pinned_host_transfer=False (1084:replica_slices.py:341)
12:59:43.136 [I] Transferring arrays to host memory with options: use_replica_parallel=True, enable_pinned_host_transfer=False (1084:replica_slices.py:341)
12:59:43.148 [I] Array name: 'params.PaliGemma.llm.layers.mlp.gating_einsum.value', global shape: (18, 2, 2048, 16384), write shape: (18, 2, 2048, 16384), chosen chunk shape: (18, 2, 2048, 8192) (1084:tensorstore_utils.py:408)
12:59:43.189 [I] [process=0][thread=array_type_handler] Wrote 71 array_metadata.ArrayMetadata to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/params.orbax-checkpoint-tmp-2/array_metadatas/process_0 (1084:array_metadata_store.py:198)
12:59:45.334 [I] [process=0] /jax/checkpoint/write/blocking_bytes_per_sec: 1.4 GiB/s (total bytes: 7.2 GiB) (time elapsed: 5 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
12:59:45.337 [I] [process=0] /jax/checkpoint/write/blocking_bytes_per_sec: 711.7 MiB/s (total bytes: 3.5 GiB) (time elapsed: 5 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
12:59:45.342 [I] [process=0][thread=async_save] Background save thread started.                   (1084:async_checkpointer.py:77)
12:59:45.342 [I] Finished blocking save in 5.03 seconds. Continuing to save asynchronously to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000. (1084:async_checkpointer.py:548)
12:59:45.343 [I] [process=0][thread=MainThread][step=1000] Starting CheckpointManager Save Finalize thread=save_finalize (1084:checkpoint_manager.py:1453)
12:59:45.344 [I] [process=0][thread=save_finalize] Waiting for background save thread=async_save. (1084:async_checkpointer.py:258)
12:59:45.344 [I] {'step': 1000, 'event_type': 'save', 'directory': '/workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5', 'reached_preemption': False, 'preemption_received_at': None, 'synchronous': False, 'wait_for_prev_start_time': 1793969980.3103168, 'wait_for_prev_duration_secs': 0.00020432472229003906, 'checkpointer_blocking_start_time': 1793969980.311409, 'checkpointer_blocking_duration_secs': 5.0317113399505615, 'get_old_steps_start_time': 1793969985.34319, 'get_old_steps_duration_secs': 1.4543533325195312e-05, 'checkpoint_manager_blocking_start_time': 1793969980.3097968, 'checkpoint_manager_blocking_duration_secs': 5.0346527099609375} (1084:standard_logger.py:34)
12:59:45.381 [I] [process=0][thread=array_type_handler] Wrote 105 array_metadata.ArrayMetadata to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/train_state.orbax-checkpoint-tmp-3/array_metadatas/process_0 (1084:array_metadata_store.py:198)
12:59:48.934 [I] [process=0] /jax/checkpoint/write/bytes_per_sec: 856.7 MiB/s (total bytes: 7.2 GiB) (time elapsed: 8 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
12:59:48.934 [I] [process=0] /jax/checkpoint/write/bytes_per_sec: 414.1 MiB/s (total bytes: 3.5 GiB) (time elapsed: 8 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
12:59:48.934 [I] [process=0][thread=async_save] 5 Handler Commit operations completed.            (1084:async_checkpointer.py:87)
12:59:48.935 [I] Read Metadata={'item_handlers': None, 'metrics': {}, 'performance_metrics': {}, 'init_timestamp_nsecs': 1793969980334486312, 'commit_timestamp_nsecs': None, 'custom_metadata': {}} from /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/_CHECKPOINT_METADATA (1084:checkpoint.py:226)
12:59:48.935 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/assets.orbax-checkpoint-tmp-1 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/assets (1084:atomicity.py:289)
12:59:48.936 [I] Updated Metadata={'item_handlers': {'assets': 'openpi.training.checkpoints.CallbackHandler', 'params': 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler', 'train_state': 'orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler'}, 'metrics': {}, 'performance_metrics': {}, 'init_timestamp_nsecs': 1793969980334486312, 'commit_timestamp_nsecs': None, 'custom_metadata': {}} to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/_CHECKPOINT_METADATA (1084:checkpoint.py:245)
12:59:48.940 [I] [process=0][thread=async_save] Skipped cross-host ArrayMetadata validation because only one process is found: process_index=0. (1084:array_metadata_store.py:362)
12:59:48.946 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/train_state.orbax-checkpoint-tmp-3 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/train_state (1084:atomicity.py:289)
12:59:48.949 [I] [process=0][thread=async_save] Skipped cross-host ArrayMetadata validation because only one process is found: process_index=0. (1084:array_metadata_store.py:362)
12:59:48.956 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/params.orbax-checkpoint-tmp-2 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0/params (1084:atomicity.py:289)
12:59:48.957 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000.orbax-checkpoint-tmp-0 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000 (1084:atomicity.py:289)
12:59:48.958 [I] [process=0][thread=async_save] Finished saving checkpoint (finalized tmp dir) to `/workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000`. (1084:atomicity.py:573)
12:59:48.958 [I] Finished asynchronous save (blocking + background) in 8.65 seconds to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1000 (1084:async_checkpointer.py:412)
12:59:48.958 [I] [process=0][thread=async_save] Background save thread done.                      (1084:async_checkpointer.py:143)
12:59:48.959 [I] [process=0][thread=save_finalize] Done with waiting for background save thread=async_save. (1084:async_checkpointer.py:266)
12:59:48.959 [I] [process=0][thread=save_finalize] No errors found in background save thread=async_save. (1084:async_checkpointer.py:276)
12:59:48.959 [I] [process=0][thread=save_finalize][step=1000] CheckpointManager Save Finalize is syncing with other hosts... (1084:checkpoint_manager.py:2095)
12:59:48.959 [I] [process=0][thread=save_finalize][step=1000] CheckpointManager Save Finalize is done on all hosts. (1084:checkpoint_manager.py:2110)
12:59:52.808 [I] Progress on: 1.00kit/1.50kit rate:7.4s/it remaining:1:01:08 elapsed:2:02:07 postfix:- (1084:tqdm_logging.py:145)
13:00:06.896 [I] Progress on: 1.00kit/1.50kit rate:7.2s/it remaining:59:30 elapsed:2:02:22 postfix:- (1084:tqdm_logging.py:145)
13:00:21.020 [I] Progress on: 1.01kit/1.50kit rate:7.1s/it remaining:58:38 elapsed:2:02:36 postfix:- (1084:tqdm_logging.py:145)
13:00:35.147 [I] Progress on: 1.01kit/1.50kit rate:7.1s/it remaining:58:06 elapsed:2:02:50 postfix:- (1084:tqdm_logging.py:145)
13:00:49.251 [I] Progress on: 1.01kit/1.50kit rate:7.1s/it remaining:57:40 elapsed:2:03:04 postfix:- (1084:tqdm_logging.py:145)
13:01:03.403 [I] Progress on: 1.01kit/1.50kit rate:7.1s/it remaining:57:26 elapsed:2:03:18 postfix:- (1084:tqdm_logging.py:145)
13:01:17.548 [I] Progress on: 1.01kit/1.50kit rate:7.1s/it remaining:57:11 elapsed:2:03:32 postfix:- (1084:tqdm_logging.py:145)
13:01:31.688 [I] Progress on: 1.02kit/1.50kit rate:7.1s/it remaining:56:55 elapsed:2:03:46 postfix:- (1084:tqdm_logging.py:145)
13:01:45.829 [I] Progress on: 1.02kit/1.50kit rate:7.1s/it remaining:56:41 elapsed:2:04:01 postfix:- (1084:tqdm_logging.py:145)
13:01:59.955 [I] Progress on: 1.02kit/1.50kit rate:7.1s/it remaining:56:24 elapsed:2:04:15 postfix:- (1084:tqdm_logging.py:145)
13:02:14.113 [I] Progress on: 1.02kit/1.50kit rate:7.1s/it remaining:56:13 elapsed:2:04:29 postfix:- (1084:tqdm_logging.py:145)
13:02:28.236 [I] Progress on: 1.02kit/1.50kit rate:7.1s/it remaining:55:57 elapsed:2:04:43 postfix:- (1084:tqdm_logging.py:145)
13:02:42.404 [I] Progress on: 1.03kit/1.50kit rate:7.1s/it remaining:55:47 elapsed:2:04:57 postfix:- (1084:tqdm_logging.py:145)
13:02:56.537 [I] Progress on: 1.03kit/1.50kit rate:7.1s/it remaining:55:31 elapsed:2:05:11 postfix:- (1084:tqdm_logging.py:145)
13:03:10.664 [I] Progress on: 1.03kit/1.50kit rate:7.1s/it remaining:55:14 elapsed:2:05:25 postfix:- (1084:tqdm_logging.py:145)
13:03:24.806 [I] Progress on: 1.03kit/1.50kit rate:7.1s/it remaining:55:01 elapsed:2:05:39 postfix:- (1084:tqdm_logging.py:145)
13:03:38.971 [I] Progress on: 1.03kit/1.50kit rate:7.1s/it remaining:54:50 elapsed:2:05:54 postfix:- (1084:tqdm_logging.py:145)
13:03:53.100 [I] Progress on: 1.04kit/1.50kit rate:7.1s/it remaining:54:33 elapsed:2:06:08 postfix:- (1084:tqdm_logging.py:145)
13:04:07.206 [I] Progress on: 1.04kit/1.50kit rate:7.1s/it remaining:54:14 elapsed:2:06:22 postfix:- (1084:tqdm_logging.py:145)
13:04:21.342 [I] Progress on: 1.04kit/1.50kit rate:7.1s/it remaining:54:02 elapsed:2:06:36 postfix:- (1084:tqdm_logging.py:145)
13:04:35.479 [I] Progress on: 1.04kit/1.50kit rate:7.1s/it remaining:53:49 elapsed:2:06:50 postfix:- (1084:tqdm_logging.py:145)
13:04:49.642 [I] Progress on: 1.04kit/1.50kit rate:7.1s/it remaining:53:39 elapsed:2:07:04 postfix:- (1084:tqdm_logging.py:145)
13:05:03.802 [I] Progress on: 1.05kit/1.50kit rate:7.1s/it remaining:53:25 elapsed:2:07:18 postfix:- (1084:tqdm_logging.py:145)
13:05:17.951 [I] Progress on: 1.05kit/1.50kit rate:7.1s/it remaining:53:11 elapsed:2:07:33 postfix:- (1084:tqdm_logging.py:145)


Step 1050: grad_norm=0.0399, loss=0.0028, param_norm=1804.2487
13:05:38.808 [I] Progress on: 1.05kit/1.50kit rate:7.1s/it remaining:53:02 elapsed:2:07:53 postfix:- (1084:tqdm_logging.py:145)
13:05:53.415 [I] Progress on: 1.05kit/1.50kit rate:6.8s/it remaining:50:29 elapsed:2:08:08 postfix:- (1084:tqdm_logging.py:145)
13:06:07.563 [I] Progress on: 1.06kit/1.50kit rate:6.9s/it remaining:51:19 elapsed:2:08:22 postfix:- (1084:tqdm_logging.py:145)
13:06:21.698 [I] Progress on: 1.06kit/1.50kit rate:7.0s/it remaining:51:35 elapsed:2:08:36 postfix:- (1084:tqdm_logging.py:145)
13:06:35.845 [I] Progress on: 1.06kit/1.50kit rate:7.0s/it remaining:51:37 elapsed:2:08:51 postfix:- (1084:tqdm_logging.py:145)
13:06:50.008 [I] Progress on: 1.06kit/1.50kit rate:7.1s/it remaining:51:33 elapsed:2:09:05 postfix:- (1084:tqdm_logging.py:145)
13:07:04.133 [I] Progress on: 1.06kit/1.50kit rate:7.1s/it remaining:51:19 elapsed:2:09:19 postfix:- (1084:tqdm_logging.py:145)
13:07:18.298 [I] Progress on: 1.07kit/1.50kit rate:7.1s/it remaining:51:09 elapsed:2:09:33 postfix:- (1084:tqdm_logging.py:145)
13:07:32.450 [I] Progress on: 1.07kit/1.50kit rate:7.1s/it remaining:50:56 elapsed:2:09:47 postfix:- (1084:tqdm_logging.py:145)
13:07:46.596 [I] Progress on: 1.07kit/1.50kit rate:7.1s/it remaining:50:41 elapsed:2:10:01 postfix:- (1084:tqdm_logging.py:145)
13:08:01.338 [I] Progress on: 1.07kit/1.50kit rate:7.3s/it remaining:51:45 elapsed:2:10:16 postfix:- (1084:tqdm_logging.py:145)
13:08:14.862 [I] Progress on: 1.07kit/1.50kit rate:7.0s/it remaining:49:57 elapsed:2:10:30 postfix:- (1084:tqdm_logging.py:145)
13:08:28.981 [I] Progress on: 1.08kit/1.50kit rate:7.1s/it remaining:49:50 elapsed:2:10:44 postfix:- (1084:tqdm_logging.py:145)
13:08:43.108 [I] Progress on: 1.08kit/1.50kit rate:7.1s/it remaining:49:40 elapsed:2:10:58 postfix:- (1084:tqdm_logging.py:145)
13:08:57.224 [I] Progress on: 1.08kit/1.50kit rate:7.1s/it remaining:49:26 elapsed:2:11:12 postfix:- (1084:tqdm_logging.py:145)
13:09:11.333 [I] Progress on: 1.08kit/1.50kit rate:7.1s/it remaining:49:10 elapsed:2:11:26 postfix:- (1084:tqdm_logging.py:145)
13:09:25.443 [I] Progress on: 1.08kit/1.50kit rate:7.1s/it remaining:48:55 elapsed:2:11:40 postfix:- (1084:tqdm_logging.py:145)
13:09:39.559 [I] Progress on: 1.09kit/1.50kit rate:7.1s/it remaining:48:41 elapsed:2:11:54 postfix:- (1084:tqdm_logging.py:145)
13:09:53.704 [I] Progress on: 1.09kit/1.50kit rate:7.1s/it remaining:48:30 elapsed:2:12:08 postfix:- (1084:tqdm_logging.py:145)
13:10:07.821 [I] Progress on: 1.09kit/1.50kit rate:7.1s/it remaining:48:15 elapsed:2:12:23 postfix:- (1084:tqdm_logging.py:145)
13:10:21.931 [I] Progress on: 1.09kit/1.50kit rate:7.1s/it remaining:47:59 elapsed:2:12:37 postfix:- (1084:tqdm_logging.py:145)
13:10:36.036 [I] Progress on: 1.09kit/1.50kit rate:7.1s/it remaining:47:44 elapsed:2:12:51 postfix:- (1084:tqdm_logging.py:145)
13:10:50.166 [I] Progress on: 1.10kit/1.50kit rate:7.1s/it remaining:47:32 elapsed:2:13:05 postfix:- (1084:tqdm_logging.py:145)
13:11:04.285 [I] Progress on: 1.10kit/1.50kit rate:7.1s/it remaining:47:18 elapsed:2:13:19 postfix:- (1084:tqdm_logging.py:145)
13:11:18.425 [I] Progress on: 1.10kit/1.50kit rate:7.1s/it remaining:47:05 elapsed:2:13:33 postfix:- (1084:tqdm_logging.py:145)


Step 1100: grad_norm=0.0454, loss=0.0037, param_norm=1804.2490
13:11:32.184 [I] Progress on: 1.10kit/1.50kit rate:7.1s/it remaining:47:05 elapsed:2:13:47 postfix:- (1084:tqdm_logging.py:145)
13:11:46.753 [I] Progress on: 1.10kit/1.50kit rate:6.8s/it remaining:44:43 elapsed:2:14:01 postfix:- (1084:tqdm_logging.py:145)
13:12:00.893 [I] Progress on: 1.11kit/1.50kit rate:6.9s/it remaining:45:28 elapsed:2:14:16 postfix:- (1084:tqdm_logging.py:145)
13:12:15.019 [I] Progress on: 1.11kit/1.50kit rate:7.0s/it remaining:45:42 elapsed:2:14:30 postfix:- (1084:tqdm_logging.py:145)
13:12:29.130 [I] Progress on: 1.11kit/1.50kit rate:7.0s/it remaining:45:40 elapsed:2:14:44 postfix:- (1084:tqdm_logging.py:145)
13:12:43.249 [I] Progress on: 1.11kit/1.50kit rate:7.0s/it remaining:45:32 elapsed:2:14:58 postfix:- (1084:tqdm_logging.py:145)
13:12:57.398 [I] Progress on: 1.11kit/1.50kit rate:7.1s/it remaining:45:25 elapsed:2:15:12 postfix:- (1084:tqdm_logging.py:145)
13:13:11.509 [I] Progress on: 1.12kit/1.50kit rate:7.1s/it remaining:45:10 elapsed:2:15:26 postfix:- (1084:tqdm_logging.py:145)
13:13:25.641 [I] Progress on: 1.12kit/1.50kit rate:7.1s/it remaining:44:58 elapsed:2:15:40 postfix:- (1084:tqdm_logging.py:145)
13:13:39.754 [I] Progress on: 1.12kit/1.50kit rate:7.1s/it remaining:44:42 elapsed:2:15:54 postfix:- (1084:tqdm_logging.py:145)
13:13:53.891 [I] Progress on: 1.12kit/1.50kit rate:7.1s/it remaining:44:30 elapsed:2:16:09 postfix:- (1084:tqdm_logging.py:145)
13:14:08.016 [I] Progress on: 1.12kit/1.50kit rate:7.1s/it remaining:44:16 elapsed:2:16:23 postfix:- (1084:tqdm_logging.py:145)
13:14:22.120 [I] Progress on: 1.13kit/1.50kit rate:7.1s/it remaining:43:59 elapsed:2:16:37 postfix:- (1084:tqdm_logging.py:145)
13:14:36.253 [I] Progress on: 1.13kit/1.50kit rate:7.1s/it remaining:43:46 elapsed:2:16:51 postfix:- (1084:tqdm_logging.py:145)
13:14:50.379 [I] Progress on: 1.13kit/1.50kit rate:7.1s/it remaining:43:33 elapsed:2:17:05 postfix:- (1084:tqdm_logging.py:145)
13:15:04.513 [I] Progress on: 1.13kit/1.50kit rate:7.1s/it remaining:43:19 elapsed:2:17:19 postfix:- (1084:tqdm_logging.py:145)
13:15:18.646 [I] Progress on: 1.13kit/1.50kit rate:7.1s/it remaining:43:05 elapsed:2:17:33 postfix:- (1084:tqdm_logging.py:145)
13:15:32.765 [I] Progress on: 1.14kit/1.50kit rate:7.1s/it remaining:42:51 elapsed:2:17:47 postfix:- (1084:tqdm_logging.py:145)
13:15:46.867 [I] Progress on: 1.14kit/1.50kit rate:7.1s/it remaining:42:34 elapsed:2:18:02 postfix:- (1084:tqdm_logging.py:145)
13:16:00.991 [I] Progress on: 1.14kit/1.50kit rate:7.1s/it remaining:42:21 elapsed:2:18:16 postfix:- (1084:tqdm_logging.py:145)
13:16:15.138 [I] Progress on: 1.14kit/1.50kit rate:7.1s/it remaining:42:10 elapsed:2:18:30 postfix:- (1084:tqdm_logging.py:145)
13:16:29.265 [I] Progress on: 1.14kit/1.50kit rate:7.1s/it remaining:41:55 elapsed:2:18:44 postfix:- (1084:tqdm_logging.py:145)
13:16:43.396 [I] Progress on: 1.15kit/1.50kit rate:7.1s/it remaining:41:41 elapsed:2:18:58 postfix:- (1084:tqdm_logging.py:145)
13:16:57.501 [I] Progress on: 1.15kit/1.50kit rate:7.1s/it remaining:41:25 elapsed:2:19:12 postfix:- (1084:tqdm_logging.py:145)
13:17:11.638 [I] Progress on: 1.15kit/1.50kit rate:7.1s/it remaining:41:12 elapsed:2:19:26 postfix:- (1084:tqdm_logging.py:145)


Step 1150: grad_norm=0.0399, loss=0.0029, param_norm=1804.2496
13:17:25.396 [I] Progress on: 1.15kit/1.50kit rate:7.1s/it remaining:41:12 elapsed:2:19:40 postfix:- (1084:tqdm_logging.py:145)
13:17:39.972 [I] Progress on: 1.15kit/1.50kit rate:6.8s/it remaining:39:06 elapsed:2:19:55 postfix:- (1084:tqdm_logging.py:145)
13:17:54.090 [I] Progress on: 1.16kit/1.50kit rate:6.9s/it remaining:39:40 elapsed:2:20:09 postfix:- (1084:tqdm_logging.py:145)
13:18:08.198 [I] Progress on: 1.16kit/1.50kit rate:7.0s/it remaining:39:49 elapsed:2:20:23 postfix:- (1084:tqdm_logging.py:145)
13:18:22.332 [I] Progress on: 1.16kit/1.50kit rate:7.0s/it remaining:39:49 elapsed:2:20:37 postfix:- (1084:tqdm_logging.py:145)
13:18:36.462 [I] Progress on: 1.16kit/1.50kit rate:7.0s/it remaining:39:41 elapsed:2:20:51 postfix:- (1084:tqdm_logging.py:145)
13:18:50.579 [I] Progress on: 1.16kit/1.50kit rate:7.1s/it remaining:39:29 elapsed:2:21:05 postfix:- (1084:tqdm_logging.py:145)
13:19:04.699 [I] Progress on: 1.17kit/1.50kit rate:7.1s/it remaining:39:16 elapsed:2:21:19 postfix:- (1084:tqdm_logging.py:145)
13:19:18.851 [I] Progress on: 1.17kit/1.50kit rate:7.1s/it remaining:39:06 elapsed:2:21:34 postfix:- (1084:tqdm_logging.py:145)
13:19:32.963 [I] Progress on: 1.17kit/1.50kit rate:7.1s/it remaining:38:50 elapsed:2:21:48 postfix:- (1084:tqdm_logging.py:145)
13:19:47.097 [I] Progress on: 1.17kit/1.50kit rate:7.1s/it remaining:38:37 elapsed:2:22:02 postfix:- (1084:tqdm_logging.py:145)
13:20:01.197 [I] Progress on: 1.17kit/1.50kit rate:7.1s/it remaining:38:20 elapsed:2:22:16 postfix:- (1084:tqdm_logging.py:145)
13:20:15.342 [I] Progress on: 1.18kit/1.50kit rate:7.1s/it remaining:38:08 elapsed:2:22:30 postfix:- (1084:tqdm_logging.py:145)
13:20:29.479 [I] Progress on: 1.18kit/1.50kit rate:7.1s/it remaining:37:55 elapsed:2:22:44 postfix:- (1084:tqdm_logging.py:145)
13:20:43.604 [I] Progress on: 1.18kit/1.50kit rate:7.1s/it remaining:37:41 elapsed:2:22:58 postfix:- (1084:tqdm_logging.py:145)
13:20:57.702 [I] Progress on: 1.18kit/1.50kit rate:7.1s/it remaining:37:24 elapsed:2:23:12 postfix:- (1084:tqdm_logging.py:145)
13:21:11.826 [I] Progress on: 1.18kit/1.50kit rate:7.1s/it remaining:37:10 elapsed:2:23:27 postfix:- (1084:tqdm_logging.py:145)
13:21:25.967 [I] Progress on: 1.19kit/1.50kit rate:7.1s/it remaining:36:57 elapsed:2:23:41 postfix:- (1084:tqdm_logging.py:145)
13:21:40.112 [I] Progress on: 1.19kit/1.50kit rate:7.1s/it remaining:36:45 elapsed:2:23:55 postfix:- (1084:tqdm_logging.py:145)
13:21:54.221 [I] Progress on: 1.19kit/1.50kit rate:7.1s/it remaining:36:29 elapsed:2:24:09 postfix:- (1084:tqdm_logging.py:145)
13:22:08.351 [I] Progress on: 1.19kit/1.50kit rate:7.1s/it remaining:36:15 elapsed:2:24:23 postfix:- (1084:tqdm_logging.py:145)
13:22:22.469 [I] Progress on: 1.19kit/1.50kit rate:7.1s/it remaining:36:00 elapsed:2:24:37 postfix:- (1084:tqdm_logging.py:145)
13:22:36.590 [I] Progress on: 1.20kit/1.50kit rate:7.1s/it remaining:35:46 elapsed:2:24:51 postfix:- (1084:tqdm_logging.py:145)
13:22:50.709 [I] Progress on: 1.20kit/1.50kit rate:7.1s/it remaining:35:32 elapsed:2:25:05 postfix:- (1084:tqdm_logging.py:145)
13:23:04.842 [I] Progress on: 1.20kit/1.50kit rate:7.1s/it remaining:35:18 elapsed:2:25:20 postfix:- (1084:tqdm_logging.py:145)


Step 1200: grad_norm=0.0451, loss=0.0033, param_norm=1804.2500
13:23:18.596 [I] Progress on: 1.20kit/1.50kit rate:7.1s/it remaining:35:18 elapsed:2:25:33 postfix:- (1084:tqdm_logging.py:145)
13:23:33.171 [I] Progress on: 1.20kit/1.50kit rate:6.8s/it remaining:33:26 elapsed:2:25:48 postfix:- (1084:tqdm_logging.py:145)
13:23:47.857 [I] Progress on: 1.21kit/1.50kit rate:7.1s/it remaining:34:44 elapsed:2:26:03 postfix:- (1084:tqdm_logging.py:145)
13:24:01.427 [I] Progress on: 1.21kit/1.50kit rate:7.0s/it remaining:33:53 elapsed:2:26:16 postfix:- (1084:tqdm_logging.py:145)
13:24:15.553 [I] Progress on: 1.21kit/1.50kit rate:7.0s/it remaining:33:55 elapsed:2:26:30 postfix:- (1084:tqdm_logging.py:145)
13:24:29.680 [I] Progress on: 1.21kit/1.50kit rate:7.0s/it remaining:33:48 elapsed:2:26:44 postfix:- (1084:tqdm_logging.py:145)
13:24:43.797 [I] Progress on: 1.21kit/1.50kit rate:7.1s/it remaining:33:36 elapsed:2:26:58 postfix:- (1084:tqdm_logging.py:145)
13:24:57.927 [I] Progress on: 1.22kit/1.50kit rate:7.1s/it remaining:33:24 elapsed:2:27:13 postfix:- (1084:tqdm_logging.py:145)
13:25:12.058 [I] Progress on: 1.22kit/1.50kit rate:7.1s/it remaining:33:11 elapsed:2:27:27 postfix:- (1084:tqdm_logging.py:145)
13:25:26.171 [I] Progress on: 1.22kit/1.50kit rate:7.1s/it remaining:32:56 elapsed:2:27:41 postfix:- (1084:tqdm_logging.py:145)
13:25:40.303 [I] Progress on: 1.22kit/1.50kit rate:7.1s/it remaining:32:43 elapsed:2:27:55 postfix:- (1084:tqdm_logging.py:145)
13:25:54.425 [I] Progress on: 1.22kit/1.50kit rate:7.1s/it remaining:32:29 elapsed:2:28:09 postfix:- (1084:tqdm_logging.py:145)
13:26:08.546 [I] Progress on: 1.23kit/1.50kit rate:7.1s/it remaining:32:14 elapsed:2:28:23 postfix:- (1084:tqdm_logging.py:145)
13:26:22.672 [I] Progress on: 1.23kit/1.50kit rate:7.1s/it remaining:32:00 elapsed:2:28:37 postfix:- (1084:tqdm_logging.py:145)
13:26:36.809 [I] Progress on: 1.23kit/1.50kit rate:7.1s/it remaining:31:47 elapsed:2:28:51 postfix:- (1084:tqdm_logging.py:145)
13:26:50.915 [I] Progress on: 1.23kit/1.50kit rate:7.1s/it remaining:31:32 elapsed:2:29:06 postfix:- (1084:tqdm_logging.py:145)
13:27:05.049 [I] Progress on: 1.23kit/1.50kit rate:7.1s/it remaining:31:18 elapsed:2:29:20 postfix:- (1084:tqdm_logging.py:145)
13:27:19.173 [I] Progress on: 1.24kit/1.50kit rate:7.1s/it remaining:31:04 elapsed:2:29:34 postfix:- (1084:tqdm_logging.py:145)
13:27:33.299 [I] Progress on: 1.24kit/1.50kit rate:7.1s/it remaining:30:49 elapsed:2:29:48 postfix:- (1084:tqdm_logging.py:145)
13:27:47.440 [I] Progress on: 1.24kit/1.50kit rate:7.1s/it remaining:30:37 elapsed:2:30:02 postfix:- (1084:tqdm_logging.py:145)
13:28:01.517 [I] Progress on: 1.24kit/1.50kit rate:7.1s/it remaining:30:20 elapsed:2:30:16 postfix:- (1084:tqdm_logging.py:145)
13:28:15.629 [I] Progress on: 1.24kit/1.50kit rate:7.1s/it remaining:30:06 elapsed:2:30:30 postfix:- (1084:tqdm_logging.py:145)
13:28:29.748 [I] Progress on: 1.25kit/1.50kit rate:7.1s/it remaining:29:52 elapsed:2:30:44 postfix:- (1084:tqdm_logging.py:145)
13:28:43.874 [I] Progress on: 1.25kit/1.50kit rate:7.1s/it remaining:29:39 elapsed:2:30:59 postfix:- (1084:tqdm_logging.py:145)
13:28:57.984 [I] Progress on: 1.25kit/1.50kit rate:7.1s/it remaining:29:24 elapsed:2:31:13 postfix:- (1084:tqdm_logging.py:145)


Step 1250: grad_norm=0.0403, loss=0.0027, param_norm=1804.2506
13:29:11.772 [I] Progress on: 1.25kit/1.50kit rate:7.1s/it remaining:29:24 elapsed:2:31:26 postfix:- (1084:tqdm_logging.py:145)
13:29:26.314 [I] Progress on: 1.25kit/1.50kit rate:6.8s/it remaining:27:45 elapsed:2:31:41 postfix:- (1084:tqdm_logging.py:145)
13:29:40.448 [I] Progress on: 1.26kit/1.50kit rate:6.9s/it remaining:28:09 elapsed:2:31:55 postfix:- (1084:tqdm_logging.py:145)
13:29:54.558 [I] Progress on: 1.26kit/1.50kit rate:7.0s/it remaining:28:11 elapsed:2:32:09 postfix:- (1084:tqdm_logging.py:145)
13:30:08.699 [I] Progress on: 1.26kit/1.50kit rate:7.0s/it remaining:28:07 elapsed:2:32:23 postfix:- (1084:tqdm_logging.py:145)
13:30:22.817 [I] Progress on: 1.26kit/1.50kit rate:7.0s/it remaining:27:56 elapsed:2:32:38 postfix:- (1084:tqdm_logging.py:145)
13:30:36.952 [I] Progress on: 1.26kit/1.50kit rate:7.1s/it remaining:27:45 elapsed:2:32:52 postfix:- (1084:tqdm_logging.py:145)
13:30:51.051 [I] Progress on: 1.27kit/1.50kit rate:7.1s/it remaining:27:30 elapsed:2:33:06 postfix:- (1084:tqdm_logging.py:145)
13:31:05.194 [I] Progress on: 1.27kit/1.50kit rate:7.1s/it remaining:27:18 elapsed:2:33:20 postfix:- (1084:tqdm_logging.py:145)
13:31:19.326 [I] Progress on: 1.27kit/1.50kit rate:7.1s/it remaining:27:04 elapsed:2:33:34 postfix:- (1084:tqdm_logging.py:145)
13:31:33.449 [I] Progress on: 1.27kit/1.50kit rate:7.1s/it remaining:26:50 elapsed:2:33:48 postfix:- (1084:tqdm_logging.py:145)
13:31:47.571 [I] Progress on: 1.27kit/1.50kit rate:7.1s/it remaining:26:36 elapsed:2:34:02 postfix:- (1084:tqdm_logging.py:145)
13:32:01.704 [I] Progress on: 1.28kit/1.50kit rate:7.1s/it remaining:26:22 elapsed:2:34:16 postfix:- (1084:tqdm_logging.py:145)
13:32:15.843 [I] Progress on: 1.28kit/1.50kit rate:7.1s/it remaining:26:09 elapsed:2:34:31 postfix:- (1084:tqdm_logging.py:145)
13:32:29.954 [I] Progress on: 1.28kit/1.50kit rate:7.1s/it remaining:25:53 elapsed:2:34:45 postfix:- (1084:tqdm_logging.py:145)
13:32:44.091 [I] Progress on: 1.28kit/1.50kit rate:7.1s/it remaining:25:40 elapsed:2:34:59 postfix:- (1084:tqdm_logging.py:145)
13:32:58.218 [I] Progress on: 1.28kit/1.50kit rate:7.1s/it remaining:25:25 elapsed:2:35:13 postfix:- (1084:tqdm_logging.py:145)
13:33:12.326 [I] Progress on: 1.29kit/1.50kit rate:7.1s/it remaining:25:10 elapsed:2:35:27 postfix:- (1084:tqdm_logging.py:145)
13:33:26.467 [I] Progress on: 1.29kit/1.50kit rate:7.1s/it remaining:24:57 elapsed:2:35:41 postfix:- (1084:tqdm_logging.py:145)
13:33:40.589 [I] Progress on: 1.29kit/1.50kit rate:7.1s/it remaining:24:43 elapsed:2:35:55 postfix:- (1084:tqdm_logging.py:145)
13:33:54.719 [I] Progress on: 1.29kit/1.50kit rate:7.1s/it remaining:24:29 elapsed:2:36:09 postfix:- (1084:tqdm_logging.py:145)
13:34:08.834 [I] Progress on: 1.29kit/1.50kit rate:7.1s/it remaining:24:14 elapsed:2:36:24 postfix:- (1084:tqdm_logging.py:145)
13:34:22.944 [I] Progress on: 1.30kit/1.50kit rate:7.1s/it remaining:24:00 elapsed:2:36:38 postfix:- (1084:tqdm_logging.py:145)
13:34:37.090 [I] Progress on: 1.30kit/1.50kit rate:7.1s/it remaining:23:47 elapsed:2:36:52 postfix:- (1084:tqdm_logging.py:145)
13:34:51.223 [I] Progress on: 1.30kit/1.50kit rate:7.1s/it remaining:23:32 elapsed:2:37:06 postfix:- (1084:tqdm_logging.py:145)


Step 1300: grad_norm=0.0399, loss=0.0025, param_norm=1804.2509
13:35:04.983 [I] Progress on: 1.30kit/1.50kit rate:7.1s/it remaining:23:32 elapsed:2:37:20 postfix:- (1084:tqdm_logging.py:145)
13:35:19.535 [I] Progress on: 1.30kit/1.50kit rate:6.8s/it remaining:22:08 elapsed:2:37:34 postfix:- (1084:tqdm_logging.py:145)
13:35:33.669 [I] Progress on: 1.31kit/1.50kit rate:6.9s/it remaining:22:23 elapsed:2:37:48 postfix:- (1084:tqdm_logging.py:145)
13:35:47.776 [I] Progress on: 1.31kit/1.50kit rate:7.0s/it remaining:22:22 elapsed:2:38:02 postfix:- (1084:tqdm_logging.py:145)
13:36:01.890 [I] Progress on: 1.31kit/1.50kit rate:7.0s/it remaining:22:14 elapsed:2:38:17 postfix:- (1084:tqdm_logging.py:145)
13:36:16.044 [I] Progress on: 1.31kit/1.50kit rate:7.0s/it remaining:22:05 elapsed:2:38:31 postfix:- (1084:tqdm_logging.py:145)
13:36:30.166 [I] Progress on: 1.31kit/1.50kit rate:7.1s/it remaining:21:52 elapsed:2:38:45 postfix:- (1084:tqdm_logging.py:145)
13:36:44.311 [I] Progress on: 1.32kit/1.50kit rate:7.1s/it remaining:21:40 elapsed:2:38:59 postfix:- (1084:tqdm_logging.py:145)
13:36:58.389 [I] Progress on: 1.32kit/1.50kit rate:7.1s/it remaining:21:23 elapsed:2:39:13 postfix:- (1084:tqdm_logging.py:145)
13:37:12.542 [I] Progress on: 1.32kit/1.50kit rate:7.1s/it remaining:21:11 elapsed:2:39:27 postfix:- (1084:tqdm_logging.py:145)
13:37:26.660 [I] Progress on: 1.32kit/1.50kit rate:7.1s/it remaining:20:57 elapsed:2:39:41 postfix:- (1084:tqdm_logging.py:145)
13:37:40.774 [I] Progress on: 1.32kit/1.50kit rate:7.1s/it remaining:20:42 elapsed:2:39:55 postfix:- (1084:tqdm_logging.py:145)
13:37:54.894 [I] Progress on: 1.33kit/1.50kit rate:7.1s/it remaining:20:28 elapsed:2:40:10 postfix:- (1084:tqdm_logging.py:145)
13:38:09.012 [I] Progress on: 1.33kit/1.50kit rate:7.1s/it remaining:20:14 elapsed:2:40:24 postfix:- (1084:tqdm_logging.py:145)
13:38:23.152 [I] Progress on: 1.33kit/1.50kit rate:7.1s/it remaining:20:00 elapsed:2:40:38 postfix:- (1084:tqdm_logging.py:145)
13:38:37.289 [I] Progress on: 1.33kit/1.50kit rate:7.1s/it remaining:19:47 elapsed:2:40:52 postfix:- (1084:tqdm_logging.py:145)
13:38:51.421 [I] Progress on: 1.33kit/1.50kit rate:7.1s/it remaining:19:33 elapsed:2:41:06 postfix:- (1084:tqdm_logging.py:145)
13:39:05.537 [I] Progress on: 1.34kit/1.50kit rate:7.1s/it remaining:19:18 elapsed:2:41:20 postfix:- (1084:tqdm_logging.py:145)
13:39:19.641 [I] Progress on: 1.34kit/1.50kit rate:7.1s/it remaining:19:03 elapsed:2:41:34 postfix:- (1084:tqdm_logging.py:145)
13:39:34.393 [I] Progress on: 1.34kit/1.50kit rate:7.2s/it remaining:19:19 elapsed:2:41:49 postfix:- (1084:tqdm_logging.py:145)
13:39:47.918 [I] Progress on: 1.34kit/1.50kit rate:7.0s/it remaining:18:31 elapsed:2:42:03 postfix:- (1084:tqdm_logging.py:145)
13:40:02.047 [I] Progress on: 1.34kit/1.50kit rate:7.1s/it remaining:18:20 elapsed:2:42:17 postfix:- (1084:tqdm_logging.py:145)
13:40:16.160 [I] Progress on: 1.35kit/1.50kit rate:7.1s/it remaining:18:07 elapsed:2:42:31 postfix:- (1084:tqdm_logging.py:145)
13:40:30.305 [I] Progress on: 1.35kit/1.50kit rate:7.1s/it remaining:17:54 elapsed:2:42:45 postfix:- (1084:tqdm_logging.py:145)
13:40:44.457 [I] Progress on: 1.35kit/1.50kit rate:7.1s/it remaining:17:41 elapsed:2:42:59 postfix:- (1084:tqdm_logging.py:145)


Step 1350: grad_norm=0.0447, loss=0.0031, param_norm=1804.2512
13:40:58.198 [I] Progress on: 1.35kit/1.50kit rate:7.1s/it remaining:17:41 elapsed:2:43:13 postfix:- (1084:tqdm_logging.py:145)
13:41:12.699 [I] Progress on: 1.35kit/1.50kit rate:6.8s/it remaining:16:27 elapsed:2:43:27 postfix:- (1084:tqdm_logging.py:145)
13:41:26.842 [I] Progress on: 1.36kit/1.50kit rate:6.9s/it remaining:16:36 elapsed:2:43:42 postfix:- (1084:tqdm_logging.py:145)
13:41:40.970 [I] Progress on: 1.36kit/1.50kit rate:7.0s/it remaining:16:33 elapsed:2:43:56 postfix:- (1084:tqdm_logging.py:145)
13:41:55.090 [I] Progress on: 1.36kit/1.50kit rate:7.0s/it remaining:16:23 elapsed:2:44:10 postfix:- (1084:tqdm_logging.py:145)
13:42:09.202 [I] Progress on: 1.36kit/1.50kit rate:7.0s/it remaining:16:11 elapsed:2:44:24 postfix:- (1084:tqdm_logging.py:145)
13:42:23.349 [I] Progress on: 1.36kit/1.50kit rate:7.1s/it remaining:15:59 elapsed:2:44:38 postfix:- (1084:tqdm_logging.py:145)
13:42:37.436 [I] Progress on: 1.37kit/1.50kit rate:7.1s/it remaining:15:45 elapsed:2:44:52 postfix:- (1084:tqdm_logging.py:145)
13:42:51.544 [I] Progress on: 1.37kit/1.50kit rate:7.1s/it remaining:15:31 elapsed:2:45:06 postfix:- (1084:tqdm_logging.py:145)
13:43:05.659 [I] Progress on: 1.37kit/1.50kit rate:7.1s/it remaining:15:17 elapsed:2:45:20 postfix:- (1084:tqdm_logging.py:145)
13:43:19.772 [I] Progress on: 1.37kit/1.50kit rate:7.1s/it remaining:15:03 elapsed:2:45:34 postfix:- (1084:tqdm_logging.py:145)
13:43:33.885 [I] Progress on: 1.37kit/1.50kit rate:7.1s/it remaining:14:49 elapsed:2:45:49 postfix:- (1084:tqdm_logging.py:145)
13:43:48.032 [I] Progress on: 1.38kit/1.50kit rate:7.1s/it remaining:14:36 elapsed:2:46:03 postfix:- (1084:tqdm_logging.py:145)
13:44:02.158 [I] Progress on: 1.38kit/1.50kit rate:7.1s/it remaining:14:21 elapsed:2:46:17 postfix:- (1084:tqdm_logging.py:145)
13:44:16.278 [I] Progress on: 1.38kit/1.50kit rate:7.1s/it remaining:14:07 elapsed:2:46:31 postfix:- (1084:tqdm_logging.py:145)
13:44:30.375 [I] Progress on: 1.38kit/1.50kit rate:7.1s/it remaining:13:52 elapsed:2:46:45 postfix:- (1084:tqdm_logging.py:145)
13:44:44.526 [I] Progress on: 1.38kit/1.50kit rate:7.1s/it remaining:13:39 elapsed:2:46:59 postfix:- (1084:tqdm_logging.py:145)
13:44:58.648 [I] Progress on: 1.39kit/1.50kit rate:7.1s/it remaining:13:25 elapsed:2:47:13 postfix:- (1084:tqdm_logging.py:145)
13:45:13.085 [I] Progress on: 1.39kit/1.50kit rate:7.1s/it remaining:13:18 elapsed:2:47:28 postfix:- (1084:tqdm_logging.py:145)
13:45:27.208 [I] Progress on: 1.39kit/1.50kit rate:7.1s/it remaining:13:00 elapsed:2:47:42 postfix:- (1084:tqdm_logging.py:145)
13:45:41.352 [I] Progress on: 1.39kit/1.50kit rate:7.1s/it remaining:12:44 elapsed:2:47:56 postfix:- (1084:tqdm_logging.py:145)
13:45:55.481 [I] Progress on: 1.39kit/1.50kit rate:7.1s/it remaining:12:29 elapsed:2:48:10 postfix:- (1084:tqdm_logging.py:145)
13:46:09.605 [I] Progress on: 1.40kit/1.50kit rate:7.1s/it remaining:12:14 elapsed:2:48:24 postfix:- (1084:tqdm_logging.py:145)
13:46:23.754 [I] Progress on: 1.40kit/1.50kit rate:7.1s/it remaining:12:01 elapsed:2:48:38 postfix:- (1084:tqdm_logging.py:145)
13:46:37.848 [I] Progress on: 1.40kit/1.50kit rate:7.1s/it remaining:11:45 elapsed:2:48:53 postfix:- (1084:tqdm_logging.py:145)


Step 1400: grad_norm=0.0406, loss=0.0028, param_norm=1804.2515
13:46:51.661 [I] Progress on: 1.40kit/1.50kit rate:7.1s/it remaining:11:45 elapsed:2:49:06 postfix:- (1084:tqdm_logging.py:145)
13:47:06.253 [I] Progress on: 1.40kit/1.50kit rate:6.8s/it remaining:10:51 elapsed:2:49:21 postfix:- (1084:tqdm_logging.py:145)
13:47:20.357 [I] Progress on: 1.41kit/1.50kit rate:6.9s/it remaining:10:50 elapsed:2:49:35 postfix:- (1084:tqdm_logging.py:145)
13:47:34.456 [I] Progress on: 1.41kit/1.50kit rate:7.0s/it remaining:10:42 elapsed:2:49:49 postfix:- (1084:tqdm_logging.py:145)
13:47:48.571 [I] Progress on: 1.41kit/1.50kit rate:7.0s/it remaining:10:31 elapsed:2:50:03 postfix:- (1084:tqdm_logging.py:145)
13:48:02.686 [I] Progress on: 1.41kit/1.50kit rate:7.0s/it remaining:10:19 elapsed:2:50:17 postfix:- (1084:tqdm_logging.py:145)
13:48:16.805 [I] Progress on: 1.41kit/1.50kit rate:7.0s/it remaining:10:06 elapsed:2:50:31 postfix:- (1084:tqdm_logging.py:145)
13:48:30.931 [I] Progress on: 1.42kit/1.50kit rate:7.1s/it remaining:09:52 elapsed:2:50:46 postfix:- (1084:tqdm_logging.py:145)
13:48:45.050 [I] Progress on: 1.42kit/1.50kit rate:7.1s/it remaining:09:38 elapsed:2:51:00 postfix:- (1084:tqdm_logging.py:145)
13:48:59.166 [I] Progress on: 1.42kit/1.50kit rate:7.1s/it remaining:09:24 elapsed:2:51:14 postfix:- (1084:tqdm_logging.py:145)
13:49:13.274 [I] Progress on: 1.42kit/1.50kit rate:7.1s/it remaining:09:10 elapsed:2:51:28 postfix:- (1084:tqdm_logging.py:145)
13:49:27.401 [I] Progress on: 1.42kit/1.50kit rate:7.1s/it remaining:08:56 elapsed:2:51:42 postfix:- (1084:tqdm_logging.py:145)
13:49:41.518 [I] Progress on: 1.43kit/1.50kit rate:7.1s/it remaining:08:42 elapsed:2:51:56 postfix:- (1084:tqdm_logging.py:145)
13:49:55.657 [I] Progress on: 1.43kit/1.50kit rate:7.1s/it remaining:08:28 elapsed:2:52:10 postfix:- (1084:tqdm_logging.py:145)
13:50:09.776 [I] Progress on: 1.43kit/1.50kit rate:7.1s/it remaining:08:14 elapsed:2:52:24 postfix:- (1084:tqdm_logging.py:145)
13:50:23.893 [I] Progress on: 1.43kit/1.50kit rate:7.1s/it remaining:08:00 elapsed:2:52:39 postfix:- (1084:tqdm_logging.py:145)
13:50:38.023 [I] Progress on: 1.43kit/1.50kit rate:7.1s/it remaining:07:46 elapsed:2:52:53 postfix:- (1084:tqdm_logging.py:145)
13:50:52.152 [I] Progress on: 1.44kit/1.50kit rate:7.1s/it remaining:07:31 elapsed:2:53:07 postfix:- (1084:tqdm_logging.py:145)
13:51:06.293 [I] Progress on: 1.44kit/1.50kit rate:7.1s/it remaining:07:18 elapsed:2:53:21 postfix:- (1084:tqdm_logging.py:145)
13:51:20.434 [I] Progress on: 1.44kit/1.50kit rate:7.1s/it remaining:07:04 elapsed:2:53:35 postfix:- (1084:tqdm_logging.py:145)
13:51:34.549 [I] Progress on: 1.44kit/1.50kit rate:7.1s/it remaining:06:49 elapsed:2:53:49 postfix:- (1084:tqdm_logging.py:145)
13:51:48.666 [I] Progress on: 1.44kit/1.50kit rate:7.1s/it remaining:06:35 elapsed:2:54:03 postfix:- (1084:tqdm_logging.py:145)
13:52:02.793 [I] Progress on: 1.45kit/1.50kit rate:7.1s/it remaining:06:21 elapsed:2:54:17 postfix:- (1084:tqdm_logging.py:145)
13:52:16.944 [I] Progress on: 1.45kit/1.50kit rate:7.1s/it remaining:06:07 elapsed:2:54:32 postfix:- (1084:tqdm_logging.py:145)
13:52:31.059 [I] Progress on: 1.45kit/1.50kit rate:7.1s/it remaining:05:53 elapsed:2:54:46 postfix:- (1084:tqdm_logging.py:145)


Step 1450: grad_norm=0.0395, loss=0.0030, param_norm=1804.2515
13:52:44.807 [I] Progress on: 1.45kit/1.50kit rate:7.1s/it remaining:05:53 elapsed:2:54:59 postfix:- (1084:tqdm_logging.py:145)
13:52:59.318 [I] Progress on: 1.45kit/1.50kit rate:6.8s/it remaining:05:11 elapsed:2:55:14 postfix:- (1084:tqdm_logging.py:145)
13:53:13.449 [I] Progress on: 1.46kit/1.50kit rate:6.9s/it remaining:05:04 elapsed:2:55:28 postfix:- (1084:tqdm_logging.py:145)
13:53:27.571 [I] Progress on: 1.46kit/1.50kit rate:7.0s/it remaining:04:53 elapsed:2:55:42 postfix:- (1084:tqdm_logging.py:145)
13:53:41.716 [I] Progress on: 1.46kit/1.50kit rate:7.0s/it remaining:04:41 elapsed:2:55:56 postfix:- (1084:tqdm_logging.py:145)
13:53:55.843 [I] Progress on: 1.46kit/1.50kit rate:7.0s/it remaining:04:27 elapsed:2:56:11 postfix:- (1084:tqdm_logging.py:145)
13:54:09.968 [I] Progress on: 1.46kit/1.50kit rate:7.1s/it remaining:04:14 elapsed:2:56:25 postfix:- (1084:tqdm_logging.py:145)
13:54:24.092 [I] Progress on: 1.47kit/1.50kit rate:7.1s/it remaining:04:00 elapsed:2:56:39 postfix:- (1084:tqdm_logging.py:145)
13:54:38.200 [I] Progress on: 1.47kit/1.50kit rate:7.1s/it remaining:03:45 elapsed:2:56:53 postfix:- (1084:tqdm_logging.py:145)
13:54:52.345 [I] Progress on: 1.47kit/1.50kit rate:7.1s/it remaining:03:31 elapsed:2:57:07 postfix:- (1084:tqdm_logging.py:145)
13:55:06.455 [I] Progress on: 1.47kit/1.50kit rate:7.1s/it remaining:03:17 elapsed:2:57:21 postfix:- (1084:tqdm_logging.py:145)
13:55:21.102 [I] Progress on: 1.47kit/1.50kit rate:7.2s/it remaining:03:07 elapsed:2:57:36 postfix:- (1084:tqdm_logging.py:145)
13:55:34.704 [I] Progress on: 1.48kit/1.50kit rate:7.0s/it remaining:02:48 elapsed:2:57:49 postfix:- (1084:tqdm_logging.py:145)
13:55:48.843 [I] Progress on: 1.48kit/1.50kit rate:7.0s/it remaining:02:35 elapsed:2:58:04 postfix:- (1084:tqdm_logging.py:145)
13:56:02.974 [I] Progress on: 1.48kit/1.50kit rate:7.1s/it remaining:02:21 elapsed:2:58:18 postfix:- (1084:tqdm_logging.py:145)
13:56:17.098 [I] Progress on: 1.48kit/1.50kit rate:7.1s/it remaining:02:07 elapsed:2:58:32 postfix:- (1084:tqdm_logging.py:145)
13:56:31.235 [I] Progress on: 1.48kit/1.50kit rate:7.1s/it remaining:01:53 elapsed:2:58:46 postfix:- (1084:tqdm_logging.py:145)
13:56:45.346 [I] Progress on: 1.49kit/1.50kit rate:7.1s/it remaining:01:38 elapsed:2:59:00 postfix:- (1084:tqdm_logging.py:145)
13:56:59.466 [I] Progress on: 1.49kit/1.50kit rate:7.1s/it remaining:01:24 elapsed:2:59:14 postfix:- (1084:tqdm_logging.py:145)
13:57:13.596 [I] Progress on: 1.49kit/1.50kit rate:7.1s/it remaining:01:10 elapsed:2:59:28 postfix:- (1084:tqdm_logging.py:145)
13:57:27.712 [I] Progress on: 1.49kit/1.50kit rate:7.1s/it remaining:00:56 elapsed:2:59:42 postfix:- (1084:tqdm_logging.py:145)
13:57:41.855 [I] Progress on: 1.49kit/1.50kit rate:7.1s/it remaining:00:42 elapsed:2:59:57 postfix:- (1084:tqdm_logging.py:145)
13:57:55.993 [I] Progress on: 1.50kit/1.50kit rate:7.1s/it remaining:00:28 elapsed:3:00:11 postfix:- (1084:tqdm_logging.py:145)
13:58:10.116 [I] Progress on: 1.50kit/1.50kit rate:7.1s/it remaining:00:14 elapsed:3:00:25 postfix:- (1084:tqdm_logging.py:145)
13:58:24.240 [I] [process=0][thread=MainThread][wait_until_finished] No Save Finalize thread to wait for. Returning. (1084:checkpoint_manager.py:1987)
13:58:24.241 [I] [process=0] Saving checkpoint at step 1499                                       (1084:checkpoint_manager.py:1408)
13:58:24.242 [I] [process=0] Started async saving checkpoint to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499. (1084:async_checkpointer.py:439)
13:58:24.254 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4 (1084:atomicity.py:144)
13:58:24.260 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/params.orbax-checkpoint-tmp-6 (1084:atomicity.py:144)
13:58:24.260 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/assets.orbax-checkpoint-tmp-5 (1084:atomicity.py:144)
13:58:24.261 [I] Creating tmp directory /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/train_state.orbax-checkpoint-tmp-7 (1084:atomicity.py:144)
13:58:24.274 [I] Transferring arrays to host memory with options: use_replica_parallel=True, enable_pinned_host_transfer=False (1084:replica_slices.py:341)
13:58:33.591 [I] Transferring arrays to host memory with options: use_replica_parallel=True, enable_pinned_host_transfer=False (1084:replica_slices.py:341)
13:58:33.605 [I] Array name: 'params.PaliGemma.llm.layers.mlp.gating_einsum.value', global shape: (18, 2, 2048, 16384), write shape: (18, 2, 2048, 16384), chosen chunk shape: (18, 2, 2048, 8192) (1084:tensorstore_utils.py:408)
13:58:33.648 [I] [process=0][thread=array_type_handler] Wrote 71 array_metadata.ArrayMetadata to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/params.orbax-checkpoint-tmp-6/array_metadatas/process_0 (1084:array_metadata_store.py:198)
13:58:36.057 [I] [process=0] /jax/checkpoint/write/blocking_bytes_per_sec: 625.0 MiB/s (total bytes: 7.2 GiB) (time elapsed: 11 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
13:58:36.064 [I] [process=0] /jax/checkpoint/write/blocking_bytes_per_sec: 301.6 MiB/s (total bytes: 3.5 GiB) (time elapsed: 11 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
13:58:36.074 [I] [process=0][thread=async_save] Background save thread started.                   (1084:async_checkpointer.py:77)
13:58:36.076 [I] Finished blocking save in 11.83 seconds. Continuing to save asynchronously to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499. (1084:async_checkpointer.py:548)
13:58:36.078 [I] Preserving Checkpoint[step=1000 | time=2026-11-06 12:59:45.343153+00:00]: (Reason: on keep_period=1000). (1084:checkpoint_manager.py:1949)
13:58:36.078 [I] [process=0][thread=MainThread][step=1499] Starting CheckpointManager Save Finalize thread=save_finalize (1084:checkpoint_manager.py:1453)
13:58:36.080 [I] [process=0][thread=save_finalize] Waiting for background save thread=async_save. (1084:async_checkpointer.py:258)
13:58:36.081 [I] {'step': 1499, 'event_type': 'save', 'directory': '/workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5', 'reached_preemption': False, 'preemption_received_at': None, 'synchronous': False, 'wait_for_prev_start_time': 1793973504.2408435, 'wait_for_prev_duration_secs': 0.00017714500427246094, 'checkpointer_blocking_start_time': 1793973504.2415597, 'checkpointer_blocking_duration_secs': 11.836369276046753, 'get_old_steps_start_time': 1793973516.0779653, 'get_old_steps_duration_secs': 0.0005509853363037109, 'checkpoint_manager_blocking_start_time': 1793973504.2404659, 'checkpoint_manager_blocking_duration_secs': 11.841144323348999} (1084:standard_logger.py:34)
13:58:36.082 [I] Progress on: 1.50kit/1.50kit rate:10.6s/it remaining:00:00 elapsed:3:00:51 postfix:- (1084:tqdm_logging.py:145)
13:58:36.082 [I] Progress on: 1.50kit/1.50kit rate:10.6s/it remaining:00:00 elapsed:3:00:51 postfix:- (1084:tqdm_logging.py:145)
13:58:36.082 [I] Waiting for checkpoint manager to finish                                         (1084:train.py:275)
13:58:36.083 [I] [process=0][thread=MainThread][step=1499][wait_until_finished] Waiting for Save Finalize thread (save_finalize) to complete. (1084:checkpoint_manager.py:1998)
13:58:36.128 [I] [process=0][thread=array_type_handler] Wrote 105 array_metadata.ArrayMetadata to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/train_state.orbax-checkpoint-tmp-7/array_metadatas/process_0 (1084:array_metadata_store.py:198)
13:58:39.664 [I] [process=0] /jax/checkpoint/write/bytes_per_sec: 478.8 MiB/s (total bytes: 7.2 GiB) (time elapsed: 15 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
13:58:39.664 [I] [process=0] /jax/checkpoint/write/bytes_per_sec: 231.2 MiB/s (total bytes: 3.5 GiB) (time elapsed: 15 seconds) (per-host) (1084:base_pytree_checkpoint_handler.py:114)
13:58:39.664 [I] [process=0][thread=async_save] 5 Handler Commit operations completed.            (1084:async_checkpointer.py:87)
13:58:39.665 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/assets.orbax-checkpoint-tmp-5 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/assets (1084:atomicity.py:289)
13:58:39.667 [I] [process=0][thread=async_save] Skipped cross-host ArrayMetadata validation because only one process is found: process_index=0. (1084:array_metadata_store.py:362)
13:58:39.671 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/train_state.orbax-checkpoint-tmp-7 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/train_state (1084:atomicity.py:289)
13:58:39.672 [I] [process=0][thread=async_save] Skipped cross-host ArrayMetadata validation because only one process is found: process_index=0. (1084:array_metadata_store.py:362)
13:58:39.680 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/params.orbax-checkpoint-tmp-6 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4/params (1084:atomicity.py:289)
13:58:39.681 [I] Renaming /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499.orbax-checkpoint-tmp-4 to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499 (1084:atomicity.py:289)
13:58:39.681 [I] [process=0][thread=async_save] Finished saving checkpoint (finalized tmp dir) to `/workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499`. (1084:atomicity.py:573)
13:58:39.681 [I] Finished asynchronous save (blocking + background) in 15.44 seconds to /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499 (1084:async_checkpointer.py:412)
13:58:39.681 [I] [process=0][thread=async_save] Background save thread done.                      (1084:async_checkpointer.py:143)
13:58:39.682 [I] [process=0][thread=save_finalize] Done with waiting for background save thread=async_save. (1084:async_checkpointer.py:266)
13:58:39.682 [I] [process=0][thread=save_finalize] No errors found in background save thread=async_save. (1084:async_checkpointer.py:276)
13:58:39.682 [I] [process=0][thread=save_finalize][step=1499] CheckpointManager Save Finalize is syncing with other hosts... (1084:checkpoint_manager.py:2095)
13:58:39.682 [I] [process=0][thread=save_finalize][step=1499] CheckpointManager Save Finalize is done on all hosts. (1084:checkpoint_manager.py:2110)
13:58:39.682 [I] [process=0][thread=MainThread][step=1499][wait_until_finished] Done waiting for Save Finalize thread (save_finalize) running at step=1499. (1084:checkpoint_manager.py:2010)
```

### train_run.log

#### lab/Log22/train/train_run.log

```text
2026-11-06 10:56:59 始める
2026-11-06 13:58:44 終わった (終了コード 0)
```

### GPU の記録 gpu_train.csv（30秒おき）

#### lab/Log22/train/gpu_train.csv

```text
timestamp, name, memory.used [MiB], memory.total [MiB], utilization.gpu [%], temperature.gpu, power.draw [W]
2026/11/06 10:56:59.449, NVIDIA A40, 0 MiB, 46068 MiB, 0 %, 31, 33.73 W
2026/11/06 10:57:29.461, NVIDIA A40, 41219 MiB, 46068 MiB, 0 %, 37, 106.18 W
2026/11/06 10:57:59.477, NVIDIA A40, 41229 MiB, 46068 MiB, 0 %, 41, 108.82 W
2026/11/06 10:58:29.481, NVIDIA A40, 41229 MiB, 46068 MiB, 0 %, 43, 109.94 W
2026/11/06 10:58:59.482, NVIDIA A40, 41229 MiB, 46068 MiB, 0 %, 43, 111.37 W
2026/11/06 10:59:29.483, NVIDIA A40, 41229 MiB, 46068 MiB, 0 %, 44, 111.41 W
2026/11/06 10:59:59.577, NVIDIA A40, 41229 MiB, 46068 MiB, 0 %, 44, 111.90 W
2026/11/06 11:00:29.577, NVIDIA A40, 41265 MiB, 46068 MiB, 99 %, 59, 271.47 W
2026/11/06 11:00:59.578, NVIDIA A40, 41275 MiB, 46068 MiB, 0 %, 51, 115.00 W
2026/11/06 11:01:29.579, NVIDIA A40, 41275 MiB, 46068 MiB, 0 %, 49, 115.28 W
2026/11/06 11:01:59.580, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 57, 286.58 W
2026/11/06 11:02:29.581, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 65, 289.54 W
2026/11/06 11:02:59.581, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 70, 295.07 W
2026/11/06 11:03:29.582, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 333.70 W
2026/11/06 11:03:59.583, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 297.84 W
2026/11/06 11:04:29.583, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 292.86 W
2026/11/06 11:04:59.584, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 293.88 W
2026/11/06 11:05:29.585, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 260.08 W
2026/11/06 11:05:59.586, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 294.72 W
2026/11/06 11:06:29.587, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 290.69 W
2026/11/06 11:06:59.588, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 75, 284.61 W
2026/11/06 11:07:29.589, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 298.02 W
2026/11/06 11:07:59.590, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 294.29 W
2026/11/06 11:08:29.591, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.10 W
2026/11/06 11:08:59.592, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 303.09 W
2026/11/06 11:09:29.592, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 282.54 W
2026/11/06 11:09:59.593, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.06 W
2026/11/06 11:10:29.595, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.51 W
2026/11/06 11:10:59.596, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 300.33 W
2026/11/06 11:11:29.597, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 253.49 W
2026/11/06 11:11:59.599, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.18 W
2026/11/06 11:12:29.599, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.57 W
2026/11/06 11:12:59.600, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.28 W
2026/11/06 11:13:29.601, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 310.09 W
2026/11/06 11:13:59.602, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 296.39 W
2026/11/06 11:14:29.603, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.24 W
2026/11/06 11:14:59.605, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 304.04 W
2026/11/06 11:15:29.606, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 303.80 W
2026/11/06 11:15:59.607, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.79 W
2026/11/06 11:16:29.608, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.24 W
2026/11/06 11:16:59.608, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 304.96 W
2026/11/06 11:17:29.609, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.76 W
2026/11/06 11:17:59.610, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.08 W
2026/11/06 11:18:29.611, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.61 W
2026/11/06 11:18:59.612, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.86 W
2026/11/06 11:19:29.613, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 280.46 W
2026/11/06 11:19:59.614, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.65 W
2026/11/06 11:20:29.615, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.92 W
2026/11/06 11:20:59.616, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.65 W
2026/11/06 11:21:29.617, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 318.80 W
2026/11/06 11:21:59.617, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.19 W
2026/11/06 11:22:29.618, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 290.44 W
2026/11/06 11:22:59.620, NVIDIA A40, 41283 MiB, 46068 MiB, 99 %, 73, 303.42 W
2026/11/06 11:23:29.620, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 327.05 W
2026/11/06 11:23:59.621, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 288.38 W
2026/11/06 11:24:29.622, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 299.61 W
2026/11/06 11:24:59.622, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.78 W
2026/11/06 11:25:29.623, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 266.92 W
2026/11/06 11:25:59.624, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.44 W
2026/11/06 11:26:29.625, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 291.79 W
2026/11/06 11:26:59.626, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 283.24 W
2026/11/06 11:27:29.627, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 257.39 W
2026/11/06 11:27:59.628, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.45 W
2026/11/06 11:28:29.630, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.89 W
2026/11/06 11:28:59.630, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 313.82 W
2026/11/06 11:29:29.631, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.09 W
2026/11/06 11:29:59.632, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 295.53 W
2026/11/06 11:30:29.633, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.85 W
2026/11/06 11:30:59.634, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 282.80 W
2026/11/06 11:31:29.635, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 296.24 W
2026/11/06 11:31:59.636, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 285.10 W
2026/11/06 11:32:29.636, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 300.36 W
2026/11/06 11:32:59.637, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 292.93 W
2026/11/06 11:33:29.638, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.69 W
2026/11/06 11:33:59.639, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 290.51 W
2026/11/06 11:34:29.640, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.83 W
2026/11/06 11:34:59.641, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.00 W
2026/11/06 11:35:29.642, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 290.64 W
2026/11/06 11:35:59.642, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.46 W
2026/11/06 11:36:29.644, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 302.60 W
2026/11/06 11:36:59.646, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.32 W
2026/11/06 11:37:29.647, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 287.99 W
2026/11/06 11:37:59.648, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.73 W
2026/11/06 11:38:29.649, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.15 W
2026/11/06 11:38:59.649, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 301.40 W
2026/11/06 11:39:29.649, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 291.01 W
2026/11/06 11:39:59.651, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.09 W
2026/11/06 11:40:29.652, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 290.68 W
2026/11/06 11:40:59.653, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 263.33 W
2026/11/06 11:41:29.654, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 291.56 W
2026/11/06 11:41:59.654, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 289.25 W
2026/11/06 11:42:29.656, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.48 W
2026/11/06 11:42:59.657, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 325.90 W
2026/11/06 11:43:29.657, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 294.06 W
2026/11/06 11:43:59.658, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 291.28 W
2026/11/06 11:44:29.659, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.63 W
2026/11/06 11:44:59.659, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 322.60 W
2026/11/06 11:45:29.660, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.83 W
2026/11/06 11:45:59.662, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 288.73 W
2026/11/06 11:46:29.662, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 300.23 W
2026/11/06 11:46:59.663, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 303.00 W
2026/11/06 11:47:29.664, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.61 W
2026/11/06 11:47:59.665, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 288.63 W
2026/11/06 11:48:29.666, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 304.06 W
2026/11/06 11:48:59.666, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 313.75 W
2026/11/06 11:49:29.667, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.67 W
2026/11/06 11:49:59.668, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.53 W
2026/11/06 11:50:29.669, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 298.32 W
2026/11/06 11:50:59.671, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 324.77 W
2026/11/06 11:51:29.672, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 296.49 W
2026/11/06 11:51:59.672, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 298.49 W
2026/11/06 11:52:29.675, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 75, 304.29 W
2026/11/06 11:52:59.676, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 325.38 W
2026/11/06 11:53:29.677, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 292.51 W
2026/11/06 11:53:59.677, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 299.86 W
2026/11/06 11:54:29.678, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 299.41 W
2026/11/06 11:54:59.679, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 278.70 W
2026/11/06 11:55:29.680, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 296.19 W
2026/11/06 11:55:59.682, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.38 W
2026/11/06 11:56:29.683, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 303.59 W
2026/11/06 11:56:59.683, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 279.41 W
2026/11/06 11:57:29.684, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 295.60 W
2026/11/06 11:57:59.686, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.09 W
2026/11/06 11:58:29.687, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 287.90 W
2026/11/06 11:58:59.688, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 299.13 W
2026/11/06 11:59:29.690, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.20 W
2026/11/06 11:59:59.691, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 287.99 W
2026/11/06 12:00:29.692, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.62 W
2026/11/06 12:00:59.692, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.95 W
2026/11/06 12:01:29.693, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 290.14 W
2026/11/06 12:01:59.693, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.61 W
2026/11/06 12:02:29.695, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.33 W
2026/11/06 12:02:59.696, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 300.61 W
2026/11/06 12:03:29.697, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.69 W
2026/11/06 12:03:59.698, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 286.14 W
2026/11/06 12:04:29.699, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 302.15 W
2026/11/06 12:04:59.699, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 256.79 W
2026/11/06 12:05:29.700, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 300.51 W
2026/11/06 12:05:59.701, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.56 W
2026/11/06 12:06:29.701, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 310.67 W
2026/11/06 12:06:59.703, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 261.00 W
2026/11/06 12:07:29.703, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 297.57 W
2026/11/06 12:07:59.704, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 291.36 W
2026/11/06 12:08:29.705, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 312.27 W
2026/11/06 12:08:59.705, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 288.80 W
2026/11/06 12:09:29.706, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 283.93 W
2026/11/06 12:09:59.707, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 296.62 W
2026/11/06 12:10:29.708, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.30 W
2026/11/06 12:10:59.709, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 285.34 W
2026/11/06 12:11:29.710, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.55 W
2026/11/06 12:11:59.711, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 288.95 W
2026/11/06 12:12:29.712, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 287.81 W
2026/11/06 12:12:59.713, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.96 W
2026/11/06 12:13:29.714, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 287.40 W
2026/11/06 12:13:59.715, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 295.19 W
2026/11/06 12:14:29.716, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.34 W
2026/11/06 12:14:59.721, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 323.97 W
2026/11/06 12:15:29.722, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.97 W
2026/11/06 12:15:59.723, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 295.54 W
2026/11/06 12:16:29.724, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.93 W
2026/11/06 12:16:59.725, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 316.91 W
2026/11/06 12:17:29.726, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.06 W
2026/11/06 12:17:59.727, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 294.51 W
2026/11/06 12:18:29.728, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 299.70 W
2026/11/06 12:18:59.729, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 271.36 W
2026/11/06 12:19:29.729, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 306.47 W
2026/11/06 12:19:59.730, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 285.70 W
2026/11/06 12:20:29.731, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 303.59 W
2026/11/06 12:20:59.732, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 271.04 W
2026/11/06 12:21:29.733, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 309.10 W
2026/11/06 12:21:59.734, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 294.61 W
2026/11/06 12:22:29.735, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.02 W
2026/11/06 12:22:59.737, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 302.75 W
2026/11/06 12:23:29.738, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 275.45 W
2026/11/06 12:23:59.739, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.58 W
2026/11/06 12:24:29.740, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.48 W
2026/11/06 12:24:59.741, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 315.86 W
2026/11/06 12:25:29.742, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 304.02 W
2026/11/06 12:25:59.743, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.65 W
2026/11/06 12:26:29.744, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.58 W
2026/11/06 12:26:59.745, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 317.54 W
2026/11/06 12:27:29.746, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 301.27 W
2026/11/06 12:27:59.747, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.78 W
2026/11/06 12:28:29.748, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.73 W
2026/11/06 12:28:59.748, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 321.18 W
2026/11/06 12:29:29.749, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 285.30 W
2026/11/06 12:29:59.750, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 295.35 W
2026/11/06 12:30:29.751, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.20 W
2026/11/06 12:30:59.752, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 309.19 W
2026/11/06 12:31:29.753, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 272.88 W
2026/11/06 12:31:59.754, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.63 W
2026/11/06 12:32:29.754, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.64 W
2026/11/06 12:32:59.755, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.66 W
2026/11/06 12:33:29.756, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.77 W
2026/11/06 12:33:59.757, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.55 W
2026/11/06 12:34:29.758, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.39 W
2026/11/06 12:34:59.759, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 263.41 W
2026/11/06 12:35:29.760, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 296.44 W
2026/11/06 12:35:59.761, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 285.65 W
2026/11/06 12:36:29.762, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.52 W
2026/11/06 12:36:59.764, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 284.36 W
2026/11/06 12:37:29.765, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 287.58 W
2026/11/06 12:37:59.766, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 296.06 W
2026/11/06 12:38:29.767, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.89 W
2026/11/06 12:38:59.768, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 314.20 W
2026/11/06 12:39:29.769, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 296.52 W
2026/11/06 12:39:59.770, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 290.46 W
2026/11/06 12:40:29.772, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 303.42 W
2026/11/06 12:40:59.773, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 314.51 W
2026/11/06 12:41:29.773, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 287.35 W
2026/11/06 12:41:59.775, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 290.28 W
2026/11/06 12:42:29.776, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.82 W
2026/11/06 12:42:59.777, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 301.15 W
2026/11/06 12:43:29.778, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 296.51 W
2026/11/06 12:43:59.779, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 292.41 W
2026/11/06 12:44:29.780, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 308.45 W
2026/11/06 12:44:59.782, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 321.59 W
2026/11/06 12:45:29.783, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.48 W
2026/11/06 12:45:59.783, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.24 W
2026/11/06 12:46:29.784, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 286.80 W
2026/11/06 12:46:59.788, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.20 W
2026/11/06 12:47:29.789, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 290.42 W
2026/11/06 12:47:59.791, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 291.82 W
2026/11/06 12:48:29.792, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 304.21 W
2026/11/06 12:48:59.793, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.65 W
2026/11/06 12:49:29.794, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 287.79 W
2026/11/06 12:49:59.795, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.24 W
2026/11/06 12:50:29.796, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 290.34 W
2026/11/06 12:50:59.796, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 262.90 W
2026/11/06 12:51:29.798, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.73 W
2026/11/06 12:51:59.798, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.64 W
2026/11/06 12:52:29.799, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.05 W
2026/11/06 12:52:59.800, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 314.81 W
2026/11/06 12:53:29.801, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 294.30 W
2026/11/06 12:53:59.803, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 295.24 W
2026/11/06 12:54:29.804, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 306.64 W
2026/11/06 12:54:59.807, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 265.16 W
2026/11/06 12:55:29.808, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.02 W
2026/11/06 12:55:59.809, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.23 W
2026/11/06 12:56:29.810, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.55 W
2026/11/06 12:56:59.812, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.19 W
2026/11/06 12:57:29.813, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 297.77 W
2026/11/06 12:57:59.814, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 289.95 W
2026/11/06 12:58:29.814, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 289.50 W
2026/11/06 12:58:59.816, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 304.12 W
2026/11/06 12:59:29.817, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.87 W
2026/11/06 12:59:59.818, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.57 W
2026/11/06 13:00:29.819, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 321.16 W
2026/11/06 13:00:59.820, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 293.16 W
2026/11/06 13:01:29.821, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 295.70 W
2026/11/06 13:01:59.823, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 296.06 W
2026/11/06 13:02:29.823, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 302.96 W
2026/11/06 13:02:59.824, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 75, 299.54 W
2026/11/06 13:03:29.824, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 294.75 W
2026/11/06 13:03:59.826, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 290.00 W
2026/11/06 13:04:29.827, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 75, 297.42 W
2026/11/06 13:04:59.829, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 295.14 W
2026/11/06 13:05:29.830, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 295.67 W
2026/11/06 13:05:59.831, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 75, 299.66 W
2026/11/06 13:06:29.832, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 75, 302.67 W
2026/11/06 13:06:59.832, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 296.53 W
2026/11/06 13:07:29.832, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 284.38 W
2026/11/06 13:07:59.833, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.01 W
2026/11/06 13:08:29.834, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.00 W
2026/11/06 13:08:59.835, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 306.98 W
2026/11/06 13:09:29.836, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.97 W
2026/11/06 13:09:59.837, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.20 W
2026/11/06 13:10:29.838, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 297.38 W
2026/11/06 13:10:59.839, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 282.75 W
2026/11/06 13:11:29.840, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.61 W
2026/11/06 13:11:59.841, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.52 W
2026/11/06 13:12:29.843, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 323.04 W
2026/11/06 13:12:59.844, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.09 W
2026/11/06 13:13:29.845, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.23 W
2026/11/06 13:13:59.847, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 287.63 W
2026/11/06 13:14:29.848, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 299.44 W
2026/11/06 13:14:59.849, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.89 W
2026/11/06 13:15:29.849, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.69 W
2026/11/06 13:15:59.851, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.52 W
2026/11/06 13:16:29.852, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 299.07 W
2026/11/06 13:16:59.853, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 257.70 W
2026/11/06 13:17:29.854, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.69 W
2026/11/06 13:17:59.856, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.45 W
2026/11/06 13:18:29.857, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 279.77 W
2026/11/06 13:18:59.857, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 302.08 W
2026/11/06 13:19:29.858, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.81 W
2026/11/06 13:19:59.859, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 297.33 W
2026/11/06 13:20:29.860, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 287.33 W
2026/11/06 13:20:59.861, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 302.73 W
2026/11/06 13:21:29.863, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 286.24 W
2026/11/06 13:21:59.863, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.83 W
2026/11/06 13:22:29.865, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.96 W
2026/11/06 13:22:59.865, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.01 W
2026/11/06 13:23:29.867, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 298.39 W
2026/11/06 13:23:59.867, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.52 W
2026/11/06 13:24:29.868, NVIDIA A40, 41283 MiB, 46068 MiB, 99 %, 74, 301.08 W
2026/11/06 13:24:59.869, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 300.04 W
2026/11/06 13:25:29.870, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.62 W
2026/11/06 13:25:59.871, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.73 W
2026/11/06 13:26:29.873, NVIDIA A40, 41283 MiB, 46068 MiB, 99 %, 73, 297.78 W
2026/11/06 13:26:59.874, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 302.25 W
2026/11/06 13:27:29.875, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.17 W
2026/11/06 13:27:59.876, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.06 W
2026/11/06 13:28:29.877, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 297.41 W
2026/11/06 13:28:59.879, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.49 W
2026/11/06 13:29:29.879, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.65 W
2026/11/06 13:29:59.880, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.89 W
2026/11/06 13:30:29.881, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 300.93 W
2026/11/06 13:30:59.882, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 302.25 W
2026/11/06 13:31:29.883, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 291.10 W
2026/11/06 13:31:59.884, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.51 W
2026/11/06 13:32:29.886, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 303.13 W
2026/11/06 13:32:59.887, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 254.99 W
2026/11/06 13:33:29.889, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.53 W
2026/11/06 13:33:59.890, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 292.64 W
2026/11/06 13:34:29.891, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 307.31 W
2026/11/06 13:34:59.893, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 256.73 W
2026/11/06 13:35:29.894, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 295.17 W
2026/11/06 13:35:59.895, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.61 W
2026/11/06 13:36:29.896, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 285.80 W
2026/11/06 13:36:59.897, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 318.79 W
2026/11/06 13:37:29.897, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 294.95 W
2026/11/06 13:37:59.898, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 300.02 W
2026/11/06 13:38:29.899, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 285.39 W
2026/11/06 13:38:59.900, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 301.05 W
2026/11/06 13:39:29.901, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.09 W
2026/11/06 13:39:59.902, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.39 W
2026/11/06 13:40:29.904, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.72 W
2026/11/06 13:40:59.905, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 329.84 W
2026/11/06 13:41:29.906, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 290.53 W
2026/11/06 13:41:59.908, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 294.46 W
2026/11/06 13:42:29.909, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.10 W
2026/11/06 13:42:59.910, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 301.52 W
2026/11/06 13:43:29.912, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.29 W
2026/11/06 13:43:59.913, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 295.56 W
2026/11/06 13:44:29.914, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 293.50 W
2026/11/06 13:44:59.915, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 305.78 W
2026/11/06 13:45:29.915, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 308.99 W
2026/11/06 13:45:59.915, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 293.00 W
2026/11/06 13:46:29.916, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 289.05 W
2026/11/06 13:46:59.918, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 320.27 W
2026/11/06 13:47:29.919, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.98 W
2026/11/06 13:47:59.920, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 72, 293.33 W
2026/11/06 13:48:29.921, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 299.06 W
2026/11/06 13:48:59.921, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 294.18 W
2026/11/06 13:49:29.922, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 272.15 W
2026/11/06 13:49:59.923, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 297.96 W
2026/11/06 13:50:29.924, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.68 W
2026/11/06 13:50:59.925, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.63 W
2026/11/06 13:51:29.926, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 274.83 W
2026/11/06 13:51:59.927, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.45 W
2026/11/06 13:52:29.929, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.58 W
2026/11/06 13:52:59.930, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 307.58 W
2026/11/06 13:53:29.931, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 318.69 W
2026/11/06 13:53:59.932, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.13 W
2026/11/06 13:54:29.933, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 293.94 W
2026/11/06 13:54:59.934, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 307.84 W
2026/11/06 13:55:29.935, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 314.58 W
2026/11/06 13:55:59.936, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.96 W
2026/11/06 13:56:29.937, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 295.90 W
2026/11/06 13:56:59.938, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 74, 284.91 W
2026/11/06 13:57:29.939, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 305.85 W
2026/11/06 13:57:59.939, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 292.24 W
2026/11/06 13:58:29.941, NVIDIA A40, 41283 MiB, 46068 MiB, 100 %, 73, 296.97 W
2026/11/06 13:58:59.942, NVIDIA A40, 0 MiB, 46068 MiB, 0 %, 51, 39.72 W
2026/11/06 13:59:29.943, NVIDIA A40, 0 MiB, 46068 MiB, 0 %, 42, 37.04 W
```

### Pod の中のチェックポイントの一覧 pod_checkpoints.tsv

#### lab/Log22/train/pod_checkpoints.tsv

```text
1000/_CHECKPOINT_METADATA	426
1000/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v5/norm_stats.json	1766
1000/params/_METADATA	33052
1000/params/_sharding	17773
1000/params/array_metadatas/process_0	12891
1000/params/d/11de514fa94fa7397f3b16c7989757b5	2802
1000/params/manifest.ocdbt	118
1000/params/ocdbt.process_0/d/01c7838031cb784824f6e85768f3da4c	1244
1000/params/ocdbt.process_0/d/19236a8a0c1f6f3b336c72e6ffe6c073	208492562
1000/params/ocdbt.process_0/d/1d8c2374ab68baf9c90bdd3a3ea310bd	235536942
1000/params/ocdbt.process_0/d/1f97b66802c79af7cbcceaa34d52db0a	497184481
1000/params/ocdbt.process_0/d/22acd354a4d05287ff45318ab394b935	945630482
1000/params/ocdbt.process_0/d/29aafe3f6906f42835b8eb30ac193ede	251453477
1000/params/ocdbt.process_0/d/2b25c98ea9cccac77681f54488f0e91f	497036534
1000/params/ocdbt.process_0/d/4d39807d68f1149ab3335489e12c5bdc	92824501
1000/params/ocdbt.process_0/d/5b88e0f9eed566d0cecbd47769e3be78	81985242
1000/params/ocdbt.process_0/d/a0054fd95b95ceaf2583e1421cab3528	79624083
1000/params/ocdbt.process_0/d/a1ad46675ef0b6eb063cc83e620a04ee	5734
1000/params/ocdbt.process_0/d/b371e135b5b74b0d998604284743380c	213
1000/params/ocdbt.process_0/d/b5329feebe1b93d9fc6fbd10f91c38ca	1376
1000/params/ocdbt.process_0/d/c6bfc091e6307b26d6240010c89d6e54	945626177
1000/params/ocdbt.process_0/d/cc6f8e4a5a27b0533ddd7c280a71e7c7	490911
1000/params/ocdbt.process_0/d/d873ce562d05137cc638b073dea21e30	58980353
1000/params/ocdbt.process_0/d/e0ad262d98e97a3878f3b23d699e0255	576386465
1000/params/ocdbt.process_0/d/e4a83d791196de24fa023ac753d978bd	89686682
1000/params/ocdbt.process_0/d/f924c3c364a809d3c56fdaed353de0af	945879519
1000/params/ocdbt.process_0/d/f9a65f1e2639fff6e730893fdafbf272	828934216
1000/params/ocdbt.process_0/manifest.ocdbt	325
1000/train_state/_METADATA	60265
1000/train_state/_sharding	27701
1000/train_state/array_metadatas/process_0	19941
1000/train_state/d/93e50e0b4ca191810cc5be23fbeec59f	3001
1000/train_state/manifest.ocdbt	118
1000/train_state/ocdbt.process_0/d/079b73cab052e94cf4451c89706b85ff	454062190
1000/train_state/ocdbt.process_0/d/097aa5e19c3b6cfa8d789fdbd7f2a587	869708
1000/train_state/ocdbt.process_0/d/2274bec425105e695b3471e3fda4cea1	651169502
1000/train_state/ocdbt.process_0/d/263d58a5fe6e2a6f957419a5c6250e64	33233009
1000/train_state/ocdbt.process_0/d/2ce9d73253114279b497b66dd23b9f00	8341482
1000/train_state/ocdbt.process_0/d/310cc967bf044e951e9472604bd3903d	53834086
1000/train_state/ocdbt.process_0/d/314659fd3dd588d8360991ee5d0e9c8a	438669410
1000/train_state/ocdbt.process_0/d/395d16649f2e3647acf6f7d73fa431d8	337
1000/train_state/ocdbt.process_0/d/4ee2257687b9a58bbc98e59008b9d7be	267067007
1000/train_state/ocdbt.process_0/d/569d6367433fb85514643cade45b559c	2070328
1000/train_state/ocdbt.process_0/d/5acd8d76944c6449636980f394b27856	171
1000/train_state/ocdbt.process_0/d/6277571ca85937c9e10dce2bfaad9bc1	494
1000/train_state/ocdbt.process_0/d/76f855d011ee2fed424ffb834923429a	64829685
1000/train_state/ocdbt.process_0/d/782468a15ef050583a9b736da843be8e	133303712
1000/train_state/ocdbt.process_0/d/7b536805fdc01b0b2f6f704b908f7902	116803
1000/train_state/ocdbt.process_0/d/87fca46d3197d49a11df20eaaf8a9390	1420
1000/train_state/ocdbt.process_0/d/aa8e79949258906109c95e17c605c367	443804965
1000/train_state/ocdbt.process_0/d/baf222f9e27a8a9591242e6ab9142b77	240749
1000/train_state/ocdbt.process_0/d/c11da4fe9f2e1865adbbf91d5ca78374	44868970
1000/train_state/ocdbt.process_0/d/d425ffb41ec0bdb2adab9341db5e84fe	459079762
1000/train_state/ocdbt.process_0/d/df2ef18dd43a1f7a61fe99731daeff20	132853716
1000/train_state/ocdbt.process_0/manifest.ocdbt	375
1499/_CHECKPOINT_METADATA	426
1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v5/norm_stats.json	1766
1499/params/_METADATA	33052
1499/params/_sharding	17773
1499/params/array_metadatas/process_0	12891
1499/params/d/831fe535caed5e14003fcb24ae2c0422	2886
1499/params/manifest.ocdbt	118
1499/params/ocdbt.process_0/d/03d71e23eb6a0f81f37c3d1993d873ff	132946719
1499/params/ocdbt.process_0/d/06501058240b6008aed08923945d19d6	213
1499/params/ocdbt.process_0/d/0888c6af2d44a265d45a61a50b3de08d	34929360
1499/params/ocdbt.process_0/d/0c8965a868ae40a71d809be7a2a07110	133017111
1499/params/ocdbt.process_0/d/17b0474fd6bc13b217a900691d5e250c	497186263
1499/params/ocdbt.process_0/d/1815fc45e07c3120daffd6b0292ff777	133038730
1499/params/ocdbt.process_0/d/277d6a30b294bca0e70e811f259ab053	986
1499/params/ocdbt.process_0/d/31e72422e1c67b1e6066bddd5e7533dc	945879576
1499/params/ocdbt.process_0/d/4faf0b06c1012ded706167896bfaa99e	118289937
1499/params/ocdbt.process_0/d/5fa9031ca6c59f2bbcb2f10a907fdce9	828933514
1499/params/ocdbt.process_0/d/65a3cffb411b4734e40ae6933f4dc0b7	152865608
1499/params/ocdbt.process_0/d/6a79a8a2badf96919478b9f5d8729bc2	88705568
1499/params/ocdbt.process_0/d/7ba3c9855401b14365cd1e9f1c127c61	5726
1499/params/ocdbt.process_0/d/81313d7db9bb4fdccdb0597cdaf51d2e	1376
1499/params/ocdbt.process_0/d/81449d843eecef14f989c9992fb07626	133036170
1499/params/ocdbt.process_0/d/8f4987bb71594ac0c5689796db0d4a34	37530685
1499/params/ocdbt.process_0/d/9549eacb13c66d3268a4fa490854c68c	1299
1499/params/ocdbt.process_0/d/ab55d383d0ff6b1805a16ec2d49237e1	58980475
1499/params/ocdbt.process_0/d/bef5ec5f81d109d2b386d88a4d6c236a	118418186
1499/params/ocdbt.process_0/d/c1580b99063234454b934fd23f22d4a3	89686717
1499/params/ocdbt.process_0/d/c47a3185c56c48f3c9201d323e0dbd7d	90308146
1499/params/ocdbt.process_0/d/d1a7833d271d97a091cb93c8a42d736b	1176
1499/params/ocdbt.process_0/d/d2ab47ee7da0dc138dca8ddb5673c6d6	118186862
1499/params/ocdbt.process_0/d/ddc1b7cbfbb22891c65eabf1fd149afb	235537045
1499/params/ocdbt.process_0/d/dfbadc827cd746aaeae49fb6a9f769db	497034770
1499/params/ocdbt.process_0/d/e89dd5c67e202966a7a39de573411c08	945630570
1499/params/ocdbt.process_0/d/ed44d2de10199b34393ba2ce0888a8cb	945626284
1499/params/ocdbt.process_0/manifest.ocdbt	642
1499/train_state/_METADATA	60265
1499/train_state/_sharding	27701
1499/train_state/array_metadatas/process_0	19941
1499/train_state/d/4a25beca30a29adeec7bbd0733e7d736	2920
1499/train_state/manifest.ocdbt	118
1499/train_state/ocdbt.process_0/d/1b3b6d0fd32297a3720399fa0b471351	438172627
1499/train_state/ocdbt.process_0/d/2d72ee1b6e38cb8ab498e8ba82e77a6e	58501745
1499/train_state/ocdbt.process_0/d/301bdc6e10edd61a62ad132e8db7ce95	83675683
1499/train_state/ocdbt.process_0/d/307127f2e2ac5c5984c7b13c25f9b84a	443557591
1499/train_state/ocdbt.process_0/d/3200402d03a2070b92b3c840d1677857	1149
1499/train_state/ocdbt.process_0/d/409f7597d9de48e26c69934d749cb437	397173747
1499/train_state/ocdbt.process_0/d/4366d5fbd30a1f38ea9cb0645d595afb	1143
1499/train_state/ocdbt.process_0/d/4823e87fbc8fdb93c076f09615505000	393140554
1499/train_state/ocdbt.process_0/d/48e50e944ca9611d8b802f7bdcc2aeac	1159
1499/train_state/ocdbt.process_0/d/49c2f1d8d66df7b27b8ea29d634136ab	52076003
1499/train_state/ocdbt.process_0/d/4f0686ffb39f790e24194d824f8045ac	1149
1499/train_state/ocdbt.process_0/d/62fc1c4a174abd0cbab8588d21bda87d	81645493
1499/train_state/ocdbt.process_0/d/67cf6615c5b6d4b461943a100238d4ba	1150
1499/train_state/ocdbt.process_0/d/8159bd1193764d0237a61f3a0dc4d71f	1147
1499/train_state/ocdbt.process_0/d/820fb23e8b4596b705df9dc1b9e73a59	9807
1499/train_state/ocdbt.process_0/d/8c6e20c7873b0b1e9eeffb1ce2cfbb72	132891832
1499/train_state/ocdbt.process_0/d/8f927c810c621d4df4b7bfcb55c51e1f	1147
1499/train_state/ocdbt.process_0/d/97c1369679e5b9d75bde6259e9f12c20	1149
1499/train_state/ocdbt.process_0/d/9a2e89858ab412c55e93c3924c150397	171
1499/train_state/ocdbt.process_0/d/a52d86b8c6bc29e816ea7870a628b959	32566895
1499/train_state/ocdbt.process_0/d/ab1a9c890c89f1dd09e1c21b55fda06f	1150
1499/train_state/ocdbt.process_0/d/b59d749da73cf66844a56e77c490cdd6	128129980
1499/train_state/ocdbt.process_0/d/ba684f7cfc930d3cd6ba7c518cc449c4	911811950
1499/train_state/ocdbt.process_0/d/bdb2192d8009550973eb4a87971059b7	1156
1499/train_state/ocdbt.process_0/d/ded9411033fcd7558ba08de55a2bfa89	1149
1499/train_state/ocdbt.process_0/d/f3af2c2d447facac52ce97c1c9bd17ad	33232508
1499/train_state/ocdbt.process_0/manifest.ocdbt	589
```

### 1499 のファイルの一覧 pod_files_1499.tsv

#### lab/Log22/train/pod_files_1499.tsv

```text
_CHECKPOINT_METADATA	426
assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v5/norm_stats.json	1766
params/_METADATA	33052
params/_sharding	17773
params/array_metadatas/process_0	12891
params/d/831fe535caed5e14003fcb24ae2c0422	2886
params/manifest.ocdbt	118
params/ocdbt.process_0/d/03d71e23eb6a0f81f37c3d1993d873ff	132946719
params/ocdbt.process_0/d/06501058240b6008aed08923945d19d6	213
params/ocdbt.process_0/d/0888c6af2d44a265d45a61a50b3de08d	34929360
params/ocdbt.process_0/d/0c8965a868ae40a71d809be7a2a07110	133017111
params/ocdbt.process_0/d/17b0474fd6bc13b217a900691d5e250c	497186263
params/ocdbt.process_0/d/1815fc45e07c3120daffd6b0292ff777	133038730
params/ocdbt.process_0/d/277d6a30b294bca0e70e811f259ab053	986
params/ocdbt.process_0/d/31e72422e1c67b1e6066bddd5e7533dc	945879576
params/ocdbt.process_0/d/4faf0b06c1012ded706167896bfaa99e	118289937
params/ocdbt.process_0/d/5fa9031ca6c59f2bbcb2f10a907fdce9	828933514
params/ocdbt.process_0/d/65a3cffb411b4734e40ae6933f4dc0b7	152865608
params/ocdbt.process_0/d/6a79a8a2badf96919478b9f5d8729bc2	88705568
params/ocdbt.process_0/d/7ba3c9855401b14365cd1e9f1c127c61	5726
params/ocdbt.process_0/d/81313d7db9bb4fdccdb0597cdaf51d2e	1376
params/ocdbt.process_0/d/81449d843eecef14f989c9992fb07626	133036170
params/ocdbt.process_0/d/8f4987bb71594ac0c5689796db0d4a34	37530685
params/ocdbt.process_0/d/9549eacb13c66d3268a4fa490854c68c	1299
params/ocdbt.process_0/d/ab55d383d0ff6b1805a16ec2d49237e1	58980475
params/ocdbt.process_0/d/bef5ec5f81d109d2b386d88a4d6c236a	118418186
params/ocdbt.process_0/d/c1580b99063234454b934fd23f22d4a3	89686717
params/ocdbt.process_0/d/c47a3185c56c48f3c9201d323e0dbd7d	90308146
params/ocdbt.process_0/d/d1a7833d271d97a091cb93c8a42d736b	1176
params/ocdbt.process_0/d/d2ab47ee7da0dc138dca8ddb5673c6d6	118186862
params/ocdbt.process_0/d/ddc1b7cbfbb22891c65eabf1fd149afb	235537045
params/ocdbt.process_0/d/dfbadc827cd746aaeae49fb6a9f769db	497034770
params/ocdbt.process_0/d/e89dd5c67e202966a7a39de573411c08	945630570
params/ocdbt.process_0/d/ed44d2de10199b34393ba2ce0888a8cb	945626284
params/ocdbt.process_0/manifest.ocdbt	642
```


## 費用：推論と学習で約328円

| 使ったもの | 時間 | 費用 |
|---|---|---|
| 推論の Pod（A40・1時間0.59ドル） | 23分21秒 | 約0.23ドル（約36円） |
| 学習の Pod（A40・1時間0.59ドル） | 188分2秒 | 約1.85ドル（約292円） |
| 合わせて | 211分23秒 | 約2.08ドル（約328円） |


## 次に実機で確かめること：corr3 は自分でブロックを持ち上げるか

corr3 の学習データには、つかんだあと肩で持ち上げる直しが入った成功の記録が、前の7本と合わせて11本あります。次の実機では、同じ手順で corr3 を動かし、次の3つを見ます。

- つかんだあと、直さなくても AI が自分で持ち上げるか（今は9回とも肩を約25°直していた）
- つかむ前の根元の向きの直し（約2°）が減るか
- 持ち上げで落ちる回が出るか。出るなら、持ち上げる前に r（爪の締め直し）を押す回と押さない回を比べる

起動は次の1行です。

```
cd ~/r2r2r-scripts && ./lab_start.sh --gpu A40 --log Log22 --model corr3
```


## この回のコード（全文）

### pi05/run_policy_client.py（commit cd2fb46）

#### pi05/run_policy_client.py

```python
#!/usr/bin/env python3
"""run_policy_client.py — π0.5 (Runpod の serve_policy) の出力で mechArm 270 を動かす Mac 側クライアント.

流れ (1 チャンクごと):
  上カメラ画像 + 実測 6 軸 + グリッパー状態 → π0.5 → 行動チャンク (10 x 7, 0.1 秒刻みの絶対角)
  → 先頭 N ステップ先 (既定 5 = 0.5 秒先) の姿勢へ単発 send_angles (学習データと同じ速度 20)
  → --via-stride N を付けると, 途中の姿勢 (N-1, 2N-1, ... ステップ) も中継点として
     順に送る (チャンクの中の順番を残すため. 既定 0 = 1 点だけ送る従来どおり)
  → グリッパーの開閉がチャンク内で切り替わるなら, その直前の姿勢まで動いてから開閉
  → 撮り直して次のチャンクへ

安全策:
  - 既定はドライラン (アームに送らない). 送るには --execute
  - --execute でも既定は 1 チャンクごとに Enter で確認 (--auto で連続)
  - 学習データで通った関節範囲 ±ENVELOPE_MARGIN_DEG の外へは出さない (クランプ)
  - 肩 j1 の上限は J1_SAFE_MAX (88°) で更に切る. これを超える指令はアームが黙って捨てる (LOG352)
  - 送る直前に最終検査. 6 関節のどれかが安全範囲の外か非有限値なら, 送信もグリッパー操作もせず
    停止 (codex R1 / C2). HOME 復帰 (h) はこの検査を通さずに送れる
  - 1 回の移動量を --max-move-deg で制限 (クランプ), j4 は J4_SAFE 範囲内
  - グリッパー操作後は GRIPPER_SLEEP 待つ (2026-10-30 依頼 35 で 2.0 → 0.8 秒. --grip-wait で戻せる)
  - --raw-dir は必須. 安全範囲は **学習に使ったデータ** から作る (別のデータだと範囲が合わない)
  - 開始時に上カメラの写り方をお手本と比べ, OK 以外ならアームに何も送らずに終了
    (ずれ / 測れない. --ignore-framing で続行). この確認が通るまで daemon にも触らない
  - 開始時のグリッパー開放も, 最初に送る目標が検査を通ってから (codex R2)
  - --state-from-target を付けると, モデルに渡す state の 6 関節を「直前に送った目標」に
    する (既定 off = 実測). 肩の不足 (ブロックを持つと毎チャンク 2.4〜2.6°) が
    チャンクごとに積み重なるのを防ぐ. 目標と実測の差が 6° を超えたチャンクは実測に戻す.
    クランプ・検査・中継点のかけ直しは今までどおり実測が基準 (変わるのは state だけ)
  - 送ったあとに残ったずれ (動きが止まってから測る. > --settle-tol) は, **同じ目標を
    もう一度送って** 詰める (送信 --settle-retries 回まで. LOG359. 0 で従来どおり
    送り直さない). 新しい目標は作らない. 動きが止まらない / 読めない / 送信が失敗した
    ときは送り直しをやめる. どの終わり方でも止めずに次のチャンクへ進む
  - 止まったときに finally でアームを動かさない. 戻すときは Enter 確認の h で明示的に送る

巻き戻しと関節の補正 (2026-10-29. 推論のあとの確認で選ぶ. b / c を押さなければ今までと同じ動き):
  - b = 巻き戻し. 直前に実行した 1 回分 (推論 1 回 か 補正 1 回) で送った目標を逆順にたどり,
    始めたときの姿勢 (推論なら measured_state) へ戻す. 戻ったらその姿勢からもう一度推論する.
    続けて b を押すと 1 回分ずつさかのぼる. グリッパーを動かした回は巻き戻さない (断って聞き直す).
    ただし つかんだ区間 (グリッパーを閉じた推論 1 回分) は 1 文字で聞いて巻き戻す (2026-10-29 依頼 27):
      y = 持っている: 逆にたどって つかんだ位置まで下ろす → 開いて置く → 始めの姿勢へ
      n = 落とした:   今の位置ですぐ開く → 逆にたどって始めの姿勢へ → 人がブロックを置き直して Enter
      その他 = やめる.  rewind 行に grasp_undo: "placed" / "dropped"
    離した区間 (グリッパーを開いた推論) と グリッパーを動かした補正 は今までどおり断る.
    log.jsonl に {"type": "rewind", "rewound": true, "chunk": 巻き戻した推論の番号, ...} を足す
    (推論の行は消さない・書き換えない)
  - c = 関節の補正. Enter を待たずに 1 文字ずつ読む (押し続けるとキーリピートで繰り返す.
    1 回送るごとに溜まったキーは捨てる). 1〜6 で関節, + / - (. / ,) で 1 刻み, [ ] で刻み
    0.5/1/2°, g でグリッパー, Enter で推論に戻る, b で取り消して始めの姿勢へ戻る.
    送り方は推論と同じ (_send_and_wait_arrival = send_angles speed 20 と到着確認) で,
    apply_limits と check_send_safe をそのまま通す (通らない 1 刻みは送らない).
    1 刻みごとに log.jsonl へ {"type": "correction", ...}
    r = 爪を開かずに閉じる指令をもう一度 (締め直し) / R = 開いてから閉じ直す (つかみ直し).
    どちらも爪が閉じているときだけ (開いていれば何もしない. 閉じるのは g). 2026-10-30 依頼 34
    1 刻みの「着いた」は, 止まるのを待ってから 動かした関節だけ を 刻みの半分 (下限 0.5°) で見る
    (表示と記録の joint_reached だけ. 送り方・再送・未到達のときの数え直しは今までどおり 3°)
    届いていないときは, 同じ関節・同じ向きの次の 1 刻みに 足りなかった分 を足す (上限は刻みの大きさ.
    move 行の carry_deg). 届いた・向きを変えた・ほかの関節を選んだら足すのをやめる. 2026-10-30 依頼 35
    R で開いてから閉じ終わるまでのコマは regrip (学習データに使わない). R だけを使った補正は
    b で巻き戻せる (つかんだ区間と同じく y=持っている / n=落とした を聞く). 依頼 35
  - --execute の間は上カメラと実測角を 1 秒 10 コマで recording/ に残す (policy_recorder.py.
    お手本と同じ形). 1 コマごとに状態 (model / rewind / correct / home / wait) を付ける.
    wait = 推論のあとの確認で入力を待っている間 (人が手でブロックを置き直すので学習データに使わない)
    h / q で終えるときに 結果 (y / n / Enter=記録しない) を聞き, {run_stem}.json に残す
  - p = ゲームパッドで補正 (2026-11-05 LOG450). tools/gamepad_teleop.py と同じ操作・送り方 (1 関節だけ送る 1 刻み・
    15° 先へ 1 回送る長押し・離したら stop) のウィンドウが開き, □ で推論に戻る. 推論と同じプロセスで動かし,
    daemon のつながりと鍵を共有する (daemon は 1 本ずつしか受けない). 守りは gamepad_teleop と同じ
    (物理可動域・J4_SAFE・j1 ≤ 90.87. 学習データの範囲ではない). 範囲の外で終えたら, 次の推論で止まりうると出す.
    間の状態は correct (学習データに使う). 爪の指令は送る瞬間に記録へ入れる. 押した操作は log.jsonl に
    {"type": "pad", ...} で 1 つずつ, 終わりに {"type": "correction", "action": "accept", "device": "gamepad"}.
    b で巻き戻せる (送った目標を逆にたどる. 爪を動かした補正は今までどおり断る)
  - 推論のあとの確認 (2026-10-31 依頼 40): 実行するのは何も打たずに Enter を押したときだけ.
    知らない入力 (R・c] ・空白だけ など) は実行せずに聞き直す. r は補正の中と同じく爪を締め直す
    (閉じているときだけ. チャンクは実行しない. log.jsonl に type=confirm / action=regrip_tighten).
    確認と結果を聞く前には, 端末にたまっているキー (位置合わせを閉じたキーなど) を捨てる.
    捨てたあとに届いたキー (押しっぱなしのリピート) は捨てられないので, 問いが出てから打つ
  - --dry-keys: ドライランでも 1 回ごとに止まり b / c を試せる (アームにも daemon にも触らない.
    仮の姿勢を HOME から動かして流れだけ確かめる. Enter で「仮に実行」= 仮の姿勢が目標へ移る)

待ち時間 (2026-10-30 依頼 35. エイイチの判断で縮めた. 本線と共有の定数は変えず, ここで上書きする):
  --send-wait 0.0 (前 1.0) / --grip-wait 0.8 (前 2.0) / --grip-open-wait 0.0 (前 2.0) / --settle-wait 0.8 (前 2.0)
  前の値に戻すには  --send-wait 1.0 --grip-wait 2.0 --grip-open-wait 2.0 --settle-wait 2.0
  送ったあとの到着の確認 (_send_and_wait_arrival の 3°) はそのまま行う. 未到達のときに続けるか止めるかは
  今までどおり (--auto は止める. 手動の推論・巻き戻しは警告を出して続ける)

終了コード: 0 = 最後まで / q / h / Ctrl-C,  1 = 安全停止・写り方の不一致・途中のエラー

前提:
  - Pi の daemon が待ち受け中 / 上カメラ (iPhone 連係) が見えている
  - Runpod で serve_policy が起動し, Mac から SSH トンネル (localhost:8000) が張れている
  - pip install msgpack "websockets>=11"

Usage:
  RAW=/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
  python3 pi05/camera_align.py --raw-dir $RAW                   # 先に上カメラの写り方を合わせる
  python3 pi05/run_policy_client.py --raw-dir $RAW              # ドライラン (予測を表示・記録するだけ)
  python3 pi05/run_policy_client.py --raw-dir $RAW --execute    # 1 チャンクごとに Enter で実行
  python3 pi05/run_policy_client.py --raw-dir $RAW --execute --auto --max-chunks 40
  python3 pi05/run_policy_client.py --raw-dir $RAW --dry-keys   # アームなしで b / c の流れを試す
記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/{stamp}/ (log.jsonl + 各チャンクの入力画像.
      --execute のときは recording/ と {run_stem}.json も)
--record を付けると recorder.py --top にも trial_start/trial_end を送り, 上カメラ動画を残す.
"""
import argparse
import json
import socket
import sys
import time
import types
from datetime import datetime
from pathlib import Path

import cv2
import numpy as np

# ---- パス (run_pick_place.py と同じ構成) ----------------------------------------
_HERE = Path(__file__).resolve().parent
_ROOT = _HERE.parent
_PLACE = _ROOT / "dynamic_tracking_limits" / "place" / "place_modules"
_DRAG = _ROOT / "dynamic_tracking_limits" / "drag_teaching"
for p in (_HERE, _ROOT, _PLACE, _DRAG):
    sys.path.insert(0, str(p))

# pymycobot スタブ (waypoint_replay 経由の import を Mac 上で通すため. アーム制御は daemon 経由のみ)
if "pymycobot" not in sys.modules:
    _pkg = types.ModuleType("pymycobot"); _mod = types.ModuleType("pymycobot.mycobot")

    class _MyCobotStub:
        def __init__(self, *a, **k):
            raise RuntimeError("MyCobot は直接使わない (daemon 経由のみ)")
    _mod.MyCobot = _MyCobotStub
    sys.modules["pymycobot"] = _pkg; sys.modules["pymycobot.mycobot"] = _mod

import camera_resolver  # noqa: E402
import waypoint_replay as wr  # noqa: E402
from camera_align import (  # noqa: E402
    FRAMING_MAX_DX, FRAMING_MAX_DY, FRAMING_MIN_RESPONSE, framing_verdict, load_reference, measure_framing,
)
from fk_lookup import J1_SAFE_MAX  # noqa: E402  88.0. 本線と二重定義しない
from j0_sweep_scan import SEND_WAIT_SEC, SPEED, _send_and_wait_arrival  # noqa: E402  本線と同じ 20 / 1.0 (待ちは下の SEND_WAIT で上書き)
from policy_recorder import PolicyRecorder, install_daemon_lock  # noqa: E402
from policy_ws import PolicyClient  # noqa: E402
from rewind_correct import (  # noqa: E402
    CORRECTION_DEFAULT_STEP_INDEX, CORRECTION_HELP, CORRECTION_STEPS, GRASP_DROPPED, GRASP_PLACED, MODE_CORRECT,
    GRASP_HELD, MODE_HOME, MODE_MODEL, MODE_REGRIP, MODE_REWIND, MODE_WAIT, correction_carry_used,
    correction_joint_reached, correction_reach_tol, correction_shortfall, next_step_index, parse_correction_key,
    rewind_waypoints, split_leg,
)
from visual_servo_pick import (  # noqa: E402
    close_daemon_socket, daemon_get_angles, daemon_send_angles, daemon_set_gripper,
    daemon_set_gripper_value, get_daemon_socket,
)

TASK = "put the red block in the white box"
IMG_LONG = 384                  # build_demo_episodes.py と同じ (学習画像の長辺)
HOME = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]
GRIP_VALUE = 20                 # run_pick_place.py の close 値
# ---- 待ち時間 (2026-10-30 依頼 35. エイイチの判断で縮めた. 「time.sleep() を短縮しない」より優先) ----
# 本線 (j0_sweep_scan.py / visual_servo_pick.py) と共有の定数は変えず, この中でだけ上書きする.
# コマンドの引数 (--send-wait / --grip-wait / --grip-open-wait / --settle-wait) で前の値に戻せる.
# 2026-10-30 の実機 (184245 / 185133 / 190659) では中継点 4 点のチャンクを送り終えるのに 10.5〜10.8 秒.
GRIPPER_SLEEP = 0.8             # 爪の開け閉めのあと Mac で待つ秒 (前 2.0. DAY175 対策の daemon 側の 2 秒は Pi のまま)
GRIP_OPEN_DAEMON_WAIT = 0.0     # 開くとき daemon が指令のあとで待つ秒 (前 2.0 = daemon_set_gripper の既定.
                                # 前は開くたびに daemon 2 + Mac 2.0 = 4 秒待っていた. 閉じる方は daemon で待たない)
SEND_WAIT = 0.0                 # 送ったあと daemon が待つ秒 (前 SEND_WAIT_SEC 1.0). この待ちは daemon の
                                # 「送信後の現在角度」の記録を遅らせるだけで, 着いたかは Mac が 3° で読んで確かめる.
                                # 0 にするのは, 0 < wait のとき Mac の待ち上限が wait + 3 秒になり, 動いている間の
                                # daemon の読み直し (最長 3.7 秒. LOG352) より短くなるため (0 なら上限 35 秒)
J4_SAFE = (-90.0, 60.0)         # run_pick_place.py の J4_SAFE_MIN/MAX
ENVELOPE_MARGIN_DEG = 10.0
SEND_EPS_DEG = 1e-6             # 送信前の検査の余裕 (小数の誤差で止まらないように)
SETTLE_POLL_SEC = 0.2           # 送り直しの前後, 関節角をこの間隔で読む (codex A2)
SETTLE_STILL_DEG = 0.2          # 続けて読んだ 2 回の差が 6 関節すべてこれ未満なら「止まった」
SETTLE_WAIT_SEC = 0.8           # 止まらなくてもここで待つのをやめ, そのときの値で判定する (前 2.0. 依頼 35)
# --state-from-target で「直前に送った目標」を渡すのをやめる差 (LOG359).
# 腕がどこかに当たって止まっているときに, 目標だけが先へ進むのを防ぐ.
STATE_TARGET_MAX_GAP_DEG = 6.0
DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
LOG_ROOT = DATA_ROOT / "runs" / "pi05_policy"
RECORDER = ("127.0.0.1", 5556)


def load_envelope(raw_dir: Path):
    """学習データで通った各関節の範囲 ± マージン. 物理可動域 (wr.JOINT_LIMITS)・J4_SAFE・J1_SAFE_MAX で更に絞る."""
    qs = [np.load(p)["state"][:, :6] for p in raw_dir.glob("*/episode.npz")]
    if not qs:
        sys.exit(f"[ERROR] 学習エピソードが見つからない (安全範囲を作れない): {raw_dir}")
    q = np.concatenate(qs)
    lo = q.min(axis=0) - ENVELOPE_MARGIN_DEG
    hi = q.max(axis=0) + ENVELOPE_MARGIN_DEG
    for k, (plo, phi) in enumerate(wr.JOINT_LIMITS):
        lo[k], hi[k] = max(lo[k], plo), min(hi[k], phi)
    lo[4], hi[4] = max(lo[4], J4_SAFE[0]), min(hi[4], J4_SAFE[1])
    # 肩 j1: wr.JOINT_LIMITS[1] の上限は +123° なので, マージンを足すと +98° まで通ってしまう.
    # 88° を超える指令はアームが黙って捨てる (LOG352) ので, ここで切る.
    hi[1] = min(hi[1], J1_SAFE_MAX)
    # codex R3: lo > hi や非有限値のまま運転しない (np.clip は例外を出さないので黙って通る).
    # lo を書き換えて辻褄を合わせるのではなく, 起動を中止する.
    bad = [k for k in range(6)
           if not (np.isfinite(lo[k]) and np.isfinite(hi[k])) or lo[k] > hi[k]]
    if bad:
        sys.exit("[ERROR] 安全範囲が不整合 (lo > hi か非有限値): "
                 + "  ".join(f"j{k}[{lo[k]:+.1f},{hi[k]:+.1f}]" for k in bad)
                 + f"\n        学習データが安全制約 (J1_SAFE_MAX={J1_SAFE_MAX:.0f}° / J4_SAFE / "
                 f"JOINT_LIMITS) と合っていない: {raw_dir}")
    return lo, hi, len(qs)


def read_angles():
    for _ in range(5):
        try:
            a = daemon_get_angles()
        except Exception:
            a = None
        if wr._is_valid_angles(a):
            return np.array([float(x) for x in a])
        time.sleep(0.1)
    return None


def grab_rgb(cap):
    for _ in range(3):            # バッファに溜まった古いフレームを捨てる
        cap.grab()
    ok, frame = cap.read()
    if not ok or frame is None:
        return None, None
    return frame_to_model(frame)


def frame_to_model(frame):
    """カメラの 1 コマ (BGR) → (モデルに渡す RGB, 保存用の BGR). 長辺 IMG_LONG に縮める."""
    h, w = frame.shape[:2]
    s = IMG_LONG / max(h, w)
    small = cv2.resize(frame, (round(w * s), round(h * s)), interpolation=cv2.INTER_AREA)
    return cv2.cvtColor(small, cv2.COLOR_BGR2RGB), small   # モデルへは RGB, 保存は BGR


def plan_chunk(actions, q, grip, n_steps):
    """チャンクから今回動かす目標を決める. 戻り値: (目標6軸, グリッパー指令 None/0/1, 使ったステップ)."""
    want = (actions[:, 6] > 0.5).astype(int)
    for f in range(min(n_steps, len(actions))):
        if want[f] != grip:
            target = q.copy() if f == 0 else actions[f - 1, :6]
            return np.asarray(target, dtype=float), int(want[f]), f
    k = min(n_steps, len(actions)) - 1
    return np.asarray(actions[k, :6], dtype=float), None, k


def apply_limits(target, q, lo, hi, max_move):
    notes = []
    t = target.copy()
    clipped = np.clip(t, lo, hi)
    for k in np.where(np.abs(clipped - t) > 1e-6)[0]:
        notes.append(f"j{k} 安全範囲でクランプ {t[k]:+.1f}→{clipped[k]:+.1f}")
    t = clipped
    d = t - q
    big = np.abs(d) > max_move
    for k in np.where(big)[0]:
        notes.append(f"j{k} 移動量 {d[k]:+.1f}° を ±{max_move:.0f}° に制限")
    t = q + np.clip(d, -max_move, max_move)
    return t, notes


def check_send_safe(target, lo, hi):
    """送る直前の最終検査 (codex R1 / C2). 違反の理由を返す (空なら送って良い).

    apply_limits は「安全範囲でクランプ → 移動量の上限」の順なので, 現在角が安全範囲の
    外にあると, クランプで範囲内に入れた目標が移動量の制限でまた範囲の外に戻る.
      j1 (codex R1): 現在 110° / 予測 100° / 上限 20° → 88° にクランプ後, 110-20 = 90° を送る
      j4 (codex C2): 現在 100° / 予測  90° / 上限 20° → 60° にクランプ後, 100-20 = 80° を送る
    j1 だけでなく 6 関節すべてを見る. ここで再クランプすると移動量の上限を破るので, 送らずに止める.
    HOME 復帰 (Enter 確認の h) はこの検査を通さない: 範囲の外にいるアームを戻す手段として残す.
    """
    if not np.all(np.isfinite(target)):
        return ["目標に非有限値が入っている: " + " ".join(f"j{k}={v}" for k, v in enumerate(target))]
    bad = []
    for k in range(6):
        if target[k] < lo[k] - SEND_EPS_DEG or target[k] > hi[k] + SEND_EPS_DEG:
            msg = f"j{k} {target[k]:+.2f}° が安全範囲 [{lo[k]:+.2f}, {hi[k]:+.2f}] の外"
            if k == 1 and target[k] > hi[k] and abs(hi[k] - J1_SAFE_MAX) < 1e-9:
                msg += (f" (上限は J1_SAFE_MAX={J1_SAFE_MAX:.0f}°. "
                        "これを超える指令はアームが黙って捨てる / LOG352)")
            elif k == 4:
                msg += f" (j4 の範囲は 学習データ ∩ J4_SAFE [{J4_SAFE[0]:+.0f}, {J4_SAFE[1]:+.0f}])"
            bad.append(msg)
    return bad


def _fmt6(a):
    return None if a is None else [round(float(v), 2) for v in a[:6]]


def read_until_still():
    """関節角が止まるまで待ってから読む (codex A2/A6).

    戻り値: (角度 or None, 止まったか, 直前に読めた角度 or None)
      1 番目は **読めたときだけ** 値が入る. 読み取りに失敗したら None で,
      古い値を代わりに返さない (A6: 失敗を隠さない). 3 番目に最後に読めた値を別に返す.

    _send_and_wait_arrival も daemon_send_angles も, 戻った直後はまだ動いている
    ことがある. その値で残りのずれを測ると, 動いている途中の姿勢で判断してしまう.
    SETTLE_POLL_SEC おきに読み, 続けて読んだ 2 回の差が 6 関節すべてで
    SETTLE_STILL_DEG 未満になったら「止まった」とみなす.
    SETTLE_WAIT_SEC まで止まらなければ打ち切り, そのときの値を still=False で返す.
    """
    prev = read_angles()
    if prev is None:
        return None, False, None
    t0 = time.time()
    while time.time() - t0 < SETTLE_WAIT_SEC:
        time.sleep(SETTLE_POLL_SEC)
        cur = read_angles()
        if cur is None:
            return None, False, prev
        if np.max(np.abs(cur[:6] - prev[:6])) < SETTLE_STILL_DEG:
            return cur, True, cur
        prev = cur
    return prev, False, prev


def plan_vias(acts, q, final_target, used, stride, lo, hi, max_move, final_raw_index=None):
    """チャンクの途中の姿勢も中継点として順に通る計画を作る (LOG359).

    今までは plan_chunk が選んだ 1 点だけを送っていたので, アームはそこへ一直線に動き,
    チャンクの中の順番が消えていた. 2026-10-22 の実機 114643 のチャンク 04 では,
    予測が「最初の 0.4 秒は持ち上げだけ (j0 はほぼ動かない) → 0.5 秒から土台 j0 が回る」
    だったのに, 上げながら回る動きになっていた (お手本は つかんだあと 1.5 秒 j0 を止めて
    持ち上げている).

    stride=0 (既定) なら中継点なし = used の 1 点だけ (従来と同じ計算).
    stride=N なら 0 始まりで N-1, 2N-1, ... のうち used より小さいステップを中継点にし,
    最後に used (plan_chunk が選んだ姿勢) を置く. 例: N=3 used=9 → 2, 5, 8, 9 /
    N=3 used=6 (7 番目でグリッパーが変わる) → 2, 5, 6.

    移動量の制限 (apply_limits) は **直前の中継点の目標から** 測る. 最初の中継点だけ
    今の実測角 q から測る. 実測ではなく目標から測るので, 送る前に全部計算できる
    (= 送り始める前に全部の中継点を check_send_safe にかけられる).

    final_raw_index は, 最後の点 (used) の raw が actions の何行目か. グリッパーが
    切り替わるチャンクでは plan_chunk が used-1 行目を返すので step とずれる (codex A1).

    戻り値: [{"step", "raw_index", "raw", "target", "notes", "delta"}] の並び
    (target は ndarray). target は計画時のもので, 送る直前に実測角からかけ直す.
    """
    steps = [k for k in range(stride - 1, used, stride)] if stride and stride > 0 else []
    steps.append(int(used))
    plan = []
    prev = np.asarray(q, dtype=float).copy()
    for k in steps:
        last = k == used
        raw = (np.asarray(final_target, dtype=float) if last
               else np.asarray(acts[k, :6], dtype=float))
        t, notes = apply_limits(raw, prev, lo, hi, max_move)
        plan.append({"step": int(k), "raw_index": (final_raw_index if last else int(k)),
                     "raw": raw, "target": t, "notes": notes, "delta": t - prev})
        prev = t
    return plan


def settle_target(target, label, tol, retries):
    """送ったあとに残ったずれを, **同じ目標をもう一度送って** 詰める (LOG359).

    2026-10-21/22 の実機 4 回 (183245 / 095251 / 100713 / 102053) で, ブロックを持って
    肩を持ち上げる指令 (j1 を小さくする向き) は 28 回すべて指令まで届かず,
    0.77〜2.80° (中央 2.00°) 低いところで止まっていた. 逆向き (下げる) は 0.5〜1° 行き過ぎる.
    到達判定は _send_and_wait_arrival(..., 3.0) の 3° なので, このずれは「着いた」と
    見なされ, モデルは実際の姿勢から次を予測する. その結果 持ち上げが毎回 2° ずつ失われ,
    ブロックがマットから離れない (102053 は上げる指令の合計 41° に対して実際は 11°).

    新しい目標は作らない. 渡された target (apply_limits と check_send_safe を通ったもの) を
    そのまま送り直すだけ. どの終わり方でも止めない (警告を出して呼び出し側が次へ進む).

    retries は **settle で送る送信回数の上限** (codex A5). 送り直しは
    _send_and_wait_arrival ではなく, その中で使っている 1 回だけ送る層
    daemon_send_angles(..., speed=SPEED, wait=SEND_WAIT) を直接呼ぶ.
    中で再送しないので送信回数を正確に数えられる (上限を超えない).
    送ったあとは read_until_still() で止まるのを待ってから測る.
    チャンク本体の送信は今までどおり _send_and_wait_arrival のまま.

    戻り値: (記録用の dict, 送信回数).  dict の reason は
      within_tol    tol 以内 (送っていない)
      converged     送り直して tol 以内に入った
      not_converged 送信回数の上限まで送っても tol を超えたまま
      not_still     SETTLE_WAIT_SEC 待っても動きが止まらない (動いている最中には送らない)
      send_failed   daemon_send_angles が例外を投げた
      read_failed   関節角を読めない (古い値で判定しない)
      disabled      --settle-retries 0 (何も読まない・何も送らない)
    どの reason でも形は同じ (final_err / joint / final_actual / last_valid_actual / tries).
    """
    out = {"reason": "disabled", "tol": tol, "sends": 0, "final_err": None, "joint": None,
           "final_actual": None, "last_valid_actual": None, "tries": []}
    if retries <= 0:                   # --settle-retries 0 で従来と同じ動き (読みも送りもしない)
        return out, 0

    tgt = np.asarray(target, dtype=float)[:6]
    tries, sends, n = [], 0, 0
    actual, still, last_valid = read_until_still()
    while True:
        if actual is None:             # 読めない値で判定しない (A6)
            reason = "read_failed"
            break
        err = np.abs(tgt - actual[:6])
        k = int(err.argmax())
        cur_err = float(err[k])
        if cur_err <= tol:
            reason = "converged" if sends else "within_tol"
            break
        if not still:                  # 動いている最中には送らない (A2)
            reason = "not_still"
            break
        if sends >= retries:
            reason = "not_converged"
            break
        n += 1
        entry = {"try": n, "max_err_before": round(cur_err, 2), "joint": k, "sends": 1,
                 "sent": True, "still": None, "max_err_after": None, "actual": None}
        sends += 1
        try:
            daemon_send_angles(tgt.tolist(), speed=SPEED, wait=SEND_WAIT)
        except Exception as e:
            entry["sent"] = False
            entry["error"] = f"{type(e).__name__}: {e}"
            tries.append(entry)
            print(f"       [SETTLE] 送り直し {n} 回目の送信に失敗: {entry['error']}")
            reason = "send_failed"
            break
        after, still, lv = read_until_still()
        if lv is not None:
            last_valid = lv
        if after is None:
            tries.append(entry)
            print(f"       [SETTLE] 送り直し {n} 回目  最大ずれ {cur_err:.2f}°（関節 j{k}）→ 読めず")
            actual = None
            reason = "read_failed"
            break
        entry["still"] = bool(still)
        after_err = float(np.abs(tgt - after[:6]).max())
        entry["max_err_after"] = round(after_err, 2)
        entry["actual"] = _fmt6(after)
        tries.append(entry)
        print(f"       [SETTLE] 送り直し {n} 回目  最大ずれ {cur_err:.2f}°（関節 j{k}）→ {after_err:.2f}°"
              + ("" if still else f"  (動きが止まらないまま {SETTLE_WAIT_SEC:.1f} 秒で打ち切り)"))
        actual = after

    out.update(reason=reason, sends=sends, tries=tries, last_valid_actual=_fmt6(last_valid))
    if actual is not None:
        err = np.abs(tgt - actual[:6])
        k = int(err.argmax())
        out.update(final_err=round(float(err[k]), 2), joint=k, final_actual=_fmt6(actual))
    if sends or reason != "within_tol":
        gloss = {"within_tol": "もともと範囲内", "converged": "詰まった",
                 "not_converged": "詰まらなかった", "not_still": "動きが止まらない",
                 "send_failed": "送信が失敗", "read_failed": "関節角を読めない"}[reason]
        tail = ("最後の最大ずれ 不明" if out["final_err"] is None
                else f"最後の最大ずれ {out['final_err']:.2f}°（関節 j{out['joint']}）")
        print(f"       [SETTLE] {reason} ({gloss})  送信 {sends} 回  {tail}")
        if reason != "converged":
            print("       [WARN] 送り直しで詰めきれませんでした (止めずに次へ進みます)")
    return out, sends


def gripper_cmd(state):
    """爪の指令と, そのあとの待ち (推論・補正・巻き戻しで共通).
    閉 = daemon_set_gripper_value(GRIP_VALUE) / 開 = daemon_set_gripper(0, wait=GRIP_OPEN_DAEMON_WAIT).
    そのあと GRIPPER_SLEEP 待つ (依頼 35 で縮めた. 引数で前の値に戻せる)."""
    if state == 1:
        daemon_set_gripper_value(GRIP_VALUE)
    else:
        daemon_set_gripper(0, wait=GRIP_OPEN_DAEMON_WAIT)
    time.sleep(GRIPPER_SLEEP)


def read_angles_once():
    """記録スレッド用: 1 回だけ読む (読めなければ None. 待たない・繰り返さない)."""
    try:
        a = daemon_get_angles()
    except Exception:
        return None
    return [float(x) for x in a] if wr._is_valid_angles(a) else None


class ArmIO:
    """巻き戻しと補正で使う送り口. --execute なら daemon, ドライラン (--dry-keys) なら仮の姿勢を動かす.

    実機での送り方は推論のチャンクと同じ _send_and_wait_arrival (send_angles speed 20 + 到着確認 3°).
    グリッパーも推論と同じ gripper_cmd (閉 = daemon_set_gripper_value(GRIP_VALUE) / 開 = daemon_set_gripper(0)
    のあと GRIPPER_SLEEP 待つ).
    """

    def __init__(self, execute, recorder=None, sim_q=None):
        self.execute = execute
        self.recorder = recorder
        self.sim_q = None if sim_q is None else np.asarray(sim_q, dtype=float).copy()

    def read(self):
        if self.execute:
            return read_angles()
        return None if self.sim_q is None else self.sim_q.copy()

    def send(self, target, tag):
        if self.execute:
            ok, _ = _send_and_wait_arrival(np.asarray(target, dtype=float).tolist(), tag, 3.0,
                                           send_wait_sec=SEND_WAIT)
            return bool(ok)
        self.sim_q = np.asarray(target, dtype=float).copy()
        print(f"       [DRY   ] {tag}: 送らない (仮の姿勢を "
              + " ".join(f"{v:+.1f}" for v in self.sim_q) + " に)")
        return True

    def read_settled(self):
        """補正の 1 刻みの到着判定用: 動きが止まるのを待ってから読む (read_until_still).
        戻り値: (角度 or None (読めない), 止まったか). 止まるのを待つのは目安 SETTLE_WAIT_SEC
        (読み取りの通信時間は含まない). ドライランは仮の姿勢をそのまま返す (止まった扱い)."""
        if self.execute:
            q, still, _ = read_until_still()
            return q, bool(still)
        return (None if self.sim_q is None else self.sim_q.copy()), True

    def gripper(self, state):
        if self.recorder is not None:
            self.recorder.gripper(state)
        if not self.execute:
            print(f"       [DRY   ] グリッパー→{'閉' if state == 1 else '開'} (送らない)")
            return
        gripper_cmd(state)


class KeyReader:
    """補正モード用: Enter を待たずに 1 文字ずつ読む (termios の cbreak).

    端末でないとき (テストでパイプから流すとき) は普通に 1 文字ずつ読む.
    flush() で溜まったキー (キーリピートで送っている間に溜まった分) を捨てる.
    """

    def __enter__(self):
        self.fd = None
        if sys.stdin.isatty():
            import termios
            import tty
            self.fd = sys.stdin.fileno()
            self.old = termios.tcgetattr(self.fd)
            tty.setcbreak(self.fd)
        return self

    def get(self):
        if self.fd is None:
            return sys.stdin.read(1)
        # 端末では os.read で 1 バイトずつ読む (Python 側の読み置きに溜めない. 溜まると
        # flush() で捨てられず, 送ったあとに余分な刻みが出る)
        import os
        import select
        b = os.read(self.fd, 1)
        if b == b"\x1b":
            # 矢印キーなど (ESC [ A ...) は丸ごと読み捨てる ('[' を刻みの切り替えと取り違えない)
            while select.select([self.fd], [], [], 0.02)[0]:
                os.read(self.fd, 8)
            return "\x1b"
        if not b:
            return ""                                   # 入力の終わり
        return b.decode("utf-8", errors="ignore") or "\x00"   # 日本語入力の途中のバイトなどは無視

    def flush(self):
        if self.fd is not None:
            import termios
            termios.tcflush(self.fd, termios.TCIFLUSH)

    def __exit__(self, *exc):
        if self.fd is not None:
            import termios
            termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old)
        return False


def plan_correction_step(cur, joint, sign, step, q, lo, hi, max_move):
    """補正の 1 刻み. cur (今の補正目標) の joint を sign*step 動かし, 推論と同じ
    apply_limits (実測 q から) と check_send_safe を通す. 戻り値: (目標, notes, unsafe)."""
    want = np.asarray(cur, dtype=float)[:6].copy()
    want[joint] += sign * step
    t, notes = apply_limits(want, np.asarray(q, dtype=float)[:6], lo, hi, max_move)
    return t, notes, check_send_safe(t, lo, hi)


REACH_TOL_DEG = 3.0        # 戻れたかの判定 (推論の到着確認 _send_and_wait_arrival(..., 3.0) と同じ)


def reached_pose(qa, pose):
    """実測 qa が pose から REACH_TOL_DEG 以内か. 戻り値: (以内か, 最大ずれ or None)."""
    if qa is None:
        return False, None
    err = float(np.max(np.abs(np.asarray(qa, dtype=float)[:6] - np.asarray(pose, dtype=float)[:6])))
    return err <= REACH_TOL_DEG, round(err, 2)


def traverse(arm, waypoints, lo, hi, max_move, tag):
    """中継点を順に送る (巻き戻し・補正の取り消し). 1 点ごとに実測から apply_limits と
    check_send_safe をかけ直す (推論の中継点と同じ). 通らない点が出たらそこで止める
    (送らない). 戻り値: (最後まで行けたか, 1 点ごとの記録)."""
    out = []
    for n, wp in enumerate(waypoints):
        qa = arm.read()
        if qa is None:
            print("       [WARN] 関節角を読めない. ここで止めます (送らない)")
            out.append({"n": n, "waypoint": _fmt6(wp), "sent": False, "why": "read_failed"})
            return False, out
        t, notes = apply_limits(np.asarray(wp, dtype=float), qa[:6], lo, hi, max_move)
        unsafe = check_send_safe(t, lo, hi)
        e = {"n": n, "waypoint": _fmt6(wp), "target": _fmt6(t), "notes": notes,
             "measured_before": _fmt6(qa), "sent": False}
        for x in notes:
            print(f"       [LIMIT] {tag} {n}: {x}")
        if unsafe:
            for u in unsafe:
                print(f"       [ABORT] {tag} {n}: {u}")
            e["unsafe"] = unsafe
            out.append(e)
            return False, out
        if np.max(np.abs(t - qa[:6])) <= 0.5:        # 推論の中継点と同じ「0.5° 以下は送らない」
            out.append(e)
            continue
        ok = arm.send(t, f"{tag}_{n:02d}")
        e.update(sent=True, arrived=bool(ok))
        print(f"       [BACK  ] {n + 1}/{len(waypoints)}  → "
              + " ".join(f"{v:+.1f}" for v in t) + ("  着いた" if ok else "  未到達"))
        if not ok:
            print("       [WARN] 未到達. j4 の発熱などを確認してください")
        out.append(e)
    return True, out


def run_rewind(entry, arm, lo, hi, max_move, recorder, grasp=None):
    """history の 1 件 (推論 1 回分 / 補正 1 回分) を巻き戻す. 戻り値は log.jsonl に足す記録.

    grasp は つかんだ区間 (グリッパーを閉じた推論) と R を使った補正 を巻き戻すときだけ渡す (依頼 27 / 35):
      GRASP_PLACED  (y = 持っている): 送った目標を逆にたどって つかんだ位置 (最後に送った目標) まで
                    下ろす → 開いてブロックを置く → 残りを逆にたどって始めの姿勢へ.
                    つかんだ位置まで下ろせなかったら開かない (宙で落とさない. 補正の取り消しと同じ決まり)
      GRASP_DROPPED (n = 落とした): 今の位置ですぐ開く (閉じた指が置き直したブロックに当たらないよう)
                    → 逆にたどって始めの姿勢へ
      GRASP_HELD    (y = 持っている. R を使った補正だけ): 閉じたまま逆にたどって始めの姿勢へ (開かない)
    送り方・範囲チェックは grasp なしの巻き戻しと同じ traverse."""
    t_start = time.time()
    rec = {"type": "rewind", "rewound": True, "kind": entry["kind"], "chunk": entry.get("chunk"),
           "correction_id": entry.get("correction_id"),
           "segment_t": [entry["t_start"], entry["t_end"]], "start_pose": _fmt6(entry["start_pose"]),
           "sent_targets": [_fmt6(x) for x in entry["sent_targets"]], "t_start": t_start}
    if grasp is not None:
        rec.update(grasp_undo=grasp, gripper_opened=False)
    q0 = arm.read()
    if q0 is None:
        print("       [WARN] 関節角を読めないので巻き戻せません")
        rec.update(finished=False, why="read_failed", t_end=time.time(), waypoints=[])
        return rec
    sent = entry["sent_targets"]
    name = ('推論 ' + str(entry['chunk']) if entry['kind'] == 'chunk' else '補正 ' + str(entry['correction_id']))
    if recorder is not None:
        recorder.set_mode(MODE_REWIND)
    opened = False
    if grasp == GRASP_PLACED:
        # つかんだ位置 = グリッパーを閉じた時点の姿勢 (閉じる前に最後に送った目標. 送っていなければ始めの姿勢)
        grasp_pose = np.asarray(sent[-1] if sent else entry["start_pose"], dtype=float)[:6]
        down = split_leg(q0[:6], grasp_pose, max_move)
        back = rewind_waypoints(sent, entry["start_pose"], current=grasp_pose, max_move=max_move)
        print(f"[REWIND] {name} (つかんだ区間) を巻き戻す: つかんだ位置 "
              + " ".join(f"{v:+.1f}" for v in grasp_pose) + " まで下ろす → 開いて置く → "
              f"中継点 {len(back)} 点 → 始めの姿勢 "
              + " ".join(f"{v:+.1f}" for v in np.asarray(entry["start_pose"])[:6]))
        completed, pts = traverse(arm, down, lo, hi, max_move, "pi05_rewind_down")
        for e in pts:
            e["leg"] = "down"
        qg = arm.read()
        reached_g, err_g = reached_pose(qg, grasp_pose)
        rec.update(grasp_pose=_fmt6(grasp_pose), reached_grasp=reached_g, grasp_err_deg=err_g)
        if completed and reached_g:
            print("[REWIND] つかんだ位置に着いたのでグリッパーを開いてブロックを置きます")
            arm.gripper(0)
            opened = True
            completed, pts2 = traverse(arm, back, lo, hi, max_move, "pi05_rewind")
            for e in pts2:
                e["leg"] = "back"
            pts += pts2
        else:
            completed = False
            print("[REWIND] [WARN] つかんだ位置まで下ろせていないので, グリッパーは閉じたままにします (落とさない)")
    else:
        if grasp == GRASP_DROPPED:
            what = "つかんだ区間" if entry["kind"] == "chunk" else "R でつかみ直した補正"
            print(f"[REWIND] {name} ({what}) を巻き戻す: 落としたので先にグリッパーを開きます")
            arm.gripper(0)
            opened = True
        wps = rewind_waypoints(sent, entry["start_pose"], current=q0[:6], max_move=max_move)
        print(f"[REWIND] {name} を巻き戻す: 中継点 {len(wps)} 点 → 始めの姿勢 "
              + " ".join(f"{v:+.1f}" for v in np.asarray(entry["start_pose"])[:6]))
        completed, pts = traverse(arm, wps, lo, hi, max_move, "pi05_rewind")
    if recorder is not None:
        recorder.set_mode(MODE_MODEL)
    qa = arm.read()
    reached, err = reached_pose(qa, entry["start_pose"])
    rec.update(finished=bool(completed and reached and (grasp in (None, GRASP_HELD) or opened)),
               traverse_completed=completed, reached_start=reached,
               start_err_deg=err, waypoints=pts, t_end=time.time(), measured_after=_fmt6(qa))
    if grasp is not None:
        rec["gripper_opened"] = opened
    if not completed:
        print("[REWIND] 途中で止めました (送れない中継点). h で HOME に戻せます")
    elif not reached:
        print(f"[REWIND] [WARN] 始めの姿勢まで戻りきっていません (最大ずれ "
              f"{'不明' if err is None else f'{err:.2f}°'} > {REACH_TOL_DEG:.0f}°). "
              "この run は補正エピソードに使いません")
    return rec


def run_correction(arm, keys, lo, hi, max_move, grip, corr_id, chunk, log_f, recorder):
    """補正モード. 戻り値: (結果 dict, 補正後のグリッパー状態).
    結果 dict の action は accept (この姿勢から推論に戻る) / cancel (取り消して戻った)."""
    start_q = arm.read()
    if start_q is None:
        print("[CORR  ] 関節角を読めないので補正に入れません")
        return None, grip
    start_q = start_q[:6].copy()
    start_grip = grip
    cur = start_q.copy()
    joint, si = 0, CORRECTION_DEFAULT_STEP_INDEX
    t_start = time.time()
    sent, n_steps = [], 0
    resync = False             # 直前の 1 刻みが未到達 → 次の刻みは実測から数える
    grip_used = False          # 補正中に一度でも g でグリッパーを動かしたか (往復でも True. codex R1-2)
    regrip_used = False        # R (つかみ直し) を使ったか. 始めと終わりはどちらも閉 → b で巻き戻せる (依頼 35)
    carry, carry_key = 0.0, None   # 届かなかった分 と それを足す (関節, 向き) (依頼 35)
    if recorder is not None:
        recorder.set_mode(MODE_CORRECT)
    print(f"[CORR  ] 補正 {corr_id} に入りました (推論 {chunk} のあと)")
    print(CORRECTION_HELP)

    def _status():
        print(f"  [補正] 関節 j{joint}  刻み {CORRECTION_STEPS[si]:.1f}°  補正目標 "
              + " ".join(f"j{k}={v:+.1f}" for k, v in enumerate(cur))
              + f"  グリッパー {'閉' if grip == 1 else '開'}")
    _status()
    while True:
        ch = keys.get()
        act = parse_correction_key(ch)
        if act is None:
            continue
        kind, val = act
        if kind == "help":
            print(CORRECTION_HELP); _status()
        elif kind == "select":
            if val != joint:
                carry, carry_key = 0.0, None     # ほかの関節を選んだら足すのをやめる (依頼 35)
            joint = val; _status()
        elif kind == "step":
            si = next_step_index(si, val); _status()
        elif kind == "move":
            qa = arm.read()
            if qa is None:
                print("  [補正] 関節角を読めない. この 1 刻みは送りません")
                keys.flush(); continue
            if resync:
                cur = qa[:6].copy()
                resync = False
                carry, carry_key = 0.0, None     # 実測から数え直すので足りない分はもう入っている
            if carry_key != (joint, val):
                carry, carry_key = 0.0, None     # 向きを変えた (+ と - / ほかの関節) ら足すのをやめる
            step = CORRECTION_STEPS[si]
            if carry > 0:
                # codex 依頼 35 ⚠️: 止まる前に打ち切って測った不足は, 入力を待つ間に解消していることがある.
                # 送る直前の実測 qa で足りない分を測り直す (届いていれば 0)
                jr_now, _ = correction_joint_reached(qa, cur, joint, correction_reach_tol(step))
                carry = correction_shortfall(qa, cur, joint, val, jr_now)
            # 依頼 35: 前の 1 刻みが届いていなければ, 足りなかった分 (上限は刻みの大きさ) を足して送る.
            # 範囲チェック (apply_limits / check_send_safe) はそのまま通す
            add = correction_carry_used(carry, step)
            t, notes, unsafe = plan_correction_step(cur, joint, val, step + add, qa, lo, hi, max_move)
            e = {"type": "correction", "action": "move", "correction_id": corr_id, "after_chunk": chunk,
                 "joint": joint, "step_deg": step, "sign": val, "carry_deg": round(add, 3), "t": time.time(),
                 "before": _fmt6(cur), "after": _fmt6(t), "measured_before": _fmt6(qa),
                 "notes": notes, "sent": False}
            if add > 0:
                print(f"  [補正] 足りなかった {add:.1f}° を足して送る (j{joint} を {val * (step + add):+.1f}°)")
            for x in notes:
                print(f"  [LIMIT] {x}")
            if unsafe:
                for u in unsafe:
                    print(f"  [補正] 送りません: {u}")
                e["unsafe"] = unsafe
            elif np.max(np.abs(t - cur)) <= 1e-6:
                print(f"  [補正] j{joint} は範囲の端なのでこれ以上動かせません")
            else:
                ok = arm.send(t, f"pi05_corr{corr_id:02d}_{n_steps:03d}")
                # 依頼 34: 「着いた」は動かした関節だけを, 刻みの半分 (下限 0.5°) で見る.
                # 送り方・再送・resync は今までどおり send の結果 (3°) で決める (表示と記録だけ)
                qs, settled = arm.read_settled()
                tol = correction_reach_tol(step)
                jr, jerr = correction_joint_reached(qs, t, joint, tol)
                e.update(sent=True, arrived=bool(ok), measured_after=_fmt6(qs),
                         joint_reached=bool(jr), joint_err_deg=jerr, reach_tol_deg=tol, settled=settled)
                e["delta"] = round(float(t[joint] - cur[joint]), 3)
                cur = t
                sent.append(t.copy())
                n_steps += 1
                if not settled and jerr is not None:
                    print(f"  [補正] (動きが止まらないまま {SETTLE_WAIT_SEC:.1f} 秒で打ち切って測りました)")
                if jerr is None:
                    print(f"  [補正] j{joint} 届いたか分からない (関節角を読めない)")
                elif jr:
                    print(f"  [補正] j{joint} 着いた (目標 {t[joint]:+.1f} / 実測 {qs[joint]:+.1f}, "
                          f"ずれ {jerr:.2f}° ≤ {tol:.2f}°)")
                else:
                    print(f"  [補正] j{joint} 届いていない (目標 {t[joint]:+.1f} / 実測 {qs[joint]:+.1f}, "
                          f"ずれ {jerr:.2f}° > {tol:.2f}°)")
                # 依頼 35: 届いていなければ, 足りなかった分を次の同じ向きの 1 刻みに足す. 届いたらやめる
                carry = correction_shortfall(qs, t, joint, val, jr)
                carry_key = (joint, val) if carry > 0 else None
                if not ok:
                    print("  [WARN] 未到達. j4 の発熱などを確認してください")
                    # codex R1/R2 A3: 次の 1 刻みは届いた所 (次の move で読む実測) から数える
                    resync = True
                _status()
            log_f.write(json.dumps(e) + "\n"); log_f.flush()
            keys.flush()       # 送っている間にキーリピートで溜まった分は捨てる (送りすぎない)
        elif kind == "grip":
            new = 0 if grip == 1 else 1
            arm.gripper(new)
            log_f.write(json.dumps({"type": "correction", "action": "gripper", "correction_id": corr_id,
                                    "after_chunk": chunk, "t": time.time(), "before": grip,
                                    "after": new}) + "\n"); log_f.flush()
            grip = new
            grip_used = True
            _status()
            keys.flush()
        elif kind == "regrip":
            # 依頼 34: r = 開かずに閉じる指令をもう一度 / R = 開く → 閉じる (その場でつかみ直す).
            # 閉じる・開くは g と同じ arm.gripper (値 GRIP_VALUE・速さ・待ち GRIPPER_SLEEP も同じ)
            action = "regrip_tighten" if val == "tighten" else "regrip_reopen"
            e = {"type": "correction", "action": action, "where": "correction", "correction_id": corr_id,
                 "after_chunk": chunk, "t": time.time(), "key": "r" if val == "tighten" else "R", "before": grip}
            if grip != 1:
                what = "締め直し" if val == "tighten" else "つかみ直し"
                print(f"  [補正] 爪が開いているので{what}はしない（閉じるのは g）")
                e.update(after=grip, sent=False, skipped="gripper_open")
            elif val == "tighten":
                print("  [補正] 爪を締め直します (開かずに閉じる指令をもう一度)")
                arm.gripper(1)
                e.update(after=1, sent=True)
            else:
                print("  [補正] 爪を開いて, 閉じ直します (つかみ直し)")
                # 依頼 35: 開いてから閉じ終わるまでのコマは regrip (学習データに使わない)
                if recorder is not None:
                    recorder.set_mode(MODE_REGRIP)
                arm.gripper(0)
                arm.gripper(1)
                if recorder is not None:
                    recorder.set_mode(MODE_CORRECT)
                e.update(after=1, sent=True)
                # 始めも終わりも閉. g の往復とは分け, b で巻き戻せるようにする (y/n を聞く. 依頼 35)
                regrip_used = True
            log_f.write(json.dumps(e) + "\n"); log_f.flush()
            _status()
            keys.flush()
        elif kind == "accept":
            res = {"type": "correction", "action": "accept", "correction_id": corr_id, "after_chunk": chunk,
                   "t_start": t_start, "t_end": time.time(), "start_pose": _fmt6(start_q),
                   "final_target": _fmt6(cur), "measured_after": _fmt6(arm.read()),
                   "steps_sent": n_steps, "grip_before": start_grip, "grip_after": grip,
                   "grip_used": grip_used, "regrip_used": regrip_used,
                   "sent_targets": [_fmt6(x) for x in sent]}
            if recorder is not None:
                recorder.set_mode(MODE_MODEL)
            print(f"[CORR  ] 補正 {corr_id} を終えました ({n_steps} 刻み). この姿勢から推論に戻ります")
            return res, grip
        elif kind == "cancel":
            print(f"[CORR  ] 補正 {corr_id} を取り消して, 始めの姿勢へ戻ります")
            if recorder is not None:
                recorder.set_mode(MODE_REWIND)
            q0 = arm.read()
            completed, pts = False, []
            if q0 is not None:
                wps = rewind_waypoints(sent, start_q, current=q0[:6], max_move=max_move) if sent else []
                completed, pts = traverse(arm, wps, lo, hi, max_move, "pi05_corr_cancel")
            else:
                print("       [WARN] 関節角を読めないので戻せません")
            qa = arm.read()
            reached, err = reached_pose(qa, start_q)
            if grip != start_grip:
                if completed and reached:
                    arm.gripper(start_grip)
                    grip = start_grip
                else:
                    # codex R1 C3: 姿勢が戻っていないのにグリッパーだけ戻さない (物を落とさない)
                    print("       [WARN] 始めの姿勢へ戻れていないので, グリッパーは今のままにします")
            if recorder is not None:
                recorder.set_mode(MODE_MODEL)
            res = {"type": "correction", "action": "cancel", "correction_id": corr_id, "after_chunk": chunk,
                   "t_start": t_start, "t_end": time.time(), "start_pose": _fmt6(start_q),
                   "steps_sent": n_steps, "finished": bool(completed and reached and grip == start_grip),
                   "traverse_completed": completed, "reached_start": reached, "start_err_deg": err,
                   "waypoints": pts, "measured_after": _fmt6(qa), "grip_after": grip,
                   "grip_used": grip_used, "regrip_used": regrip_used}
            return res, grip


class PadLog:
    """ゲームパッドの補正 (p) の記録: gamepad_teleop の 1 行を log.jsonl に {"type": "pad", ...} で書き, 手元にも残す."""

    def __init__(self, log_f, corr_id, chunk):
        self.log_f, self.corr_id, self.chunk = log_f, corr_id, chunk
        self.rows = []

    def write(self, rec):
        row = {"type": "pad", "correction_id": self.corr_id, "after_chunk": self.chunk}
        row.update(rec)
        self.rows.append(row)
        self.log_f.write(json.dumps(row, ensure_ascii=False, default=float) + "\n")
        self.log_f.flush()

    def close(self):
        pass


def _load_gamepad_teleop():
    """tools/gamepad_teleop.py を読む (p を押したときだけ. pygame を使うので). 読めなければ None."""
    # gamepad_teleop は run_policy_client を import して値と送り口を使う. 別の写しを読まないよう, 今動いているこの
    # モジュールを同じ名前で登録する (--grip-wait などで変えた待ちを補正でも使う)
    sys.modules.setdefault("run_policy_client", sys.modules[__name__])
    tools = str(_ROOT / "tools")
    if tools not in sys.path:
        sys.path.insert(0, tools)
    try:
        import gamepad_teleop as gt
    except Exception as ex:
        print(f"[PAD   ] gamepad_teleop を読めません ({type(ex).__name__}: {ex}). c (キー) で補正してください")
        return None
    return gt


def run_pad_correction(arm, grip, corr_id, chunk, log_f, recorder, lo, hi):
    """p = ゲームパッドで補正 (2026-11-05 LOG450). 戻り値は run_correction と同じ (結果 dict, 補正後のグリッパー状態).
    結果の action はいつも accept (取り消しは, 推論に戻ってから b で巻き戻す)."""
    gt = _load_gamepad_teleop()
    if gt is None:
        return None, grip
    start_q = arm.read()
    if start_q is None:
        print("[PAD   ] 関節角を読めないので補正に入れません")
        return None, grip
    start_q = np.asarray(start_q, dtype=float)[:6].copy()
    if arm.execute:
        gt.load_daemon_modules()

        class PadArm(gt.DaemonArm):
            """爪の指令を送る瞬間に記録へ入れる (build_correction_episodes は指令の時刻で 0/1 を切り替える)."""

            def gripper(self, state, gate):
                def _send():
                    if recorder is not None:
                        recorder.gripper(state)
                    if state == 1:
                        daemon_set_gripper_value(GRIP_VALUE)
                    else:
                        daemon_set_gripper(0, wait=GRIP_OPEN_DAEMON_WAIT)
                sent = gate.run(_send)
                if sent:
                    time.sleep(GRIPPER_SLEEP)
                return sent
        pad_arm = PadArm()
    else:
        pad_arm = gt.DryArm(start_q)
    plog = PadLog(log_f, corr_id, chunk)
    if recorder is not None:
        recorder.set_mode(MODE_CORRECT)
    t_start = time.time()
    print(f"[PAD   ] 補正 {corr_id} (推論 {chunk} のあと). ゲームパッドで直して, □ で推論に戻ります")
    title = f"PAD CORRECTION {corr_id}  [{'EXECUTE' if arm.execute else 'DRY'}]  square: back to policy"
    end_reason, tp, worker_done = gt.run_embedded(pad_arm, start_q, plog, grip=grip, mode_text=title)
    t_end = time.time()
    if recorder is not None:
        recorder.set_mode(MODE_MODEL)
    if not arm.execute:
        arm.sim_q = np.asarray(pad_arm.q, dtype=float).copy()
    sent = [np.asarray(r["command"], dtype=float) for r in plog.rows
            if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None]
    grip_rows = [r for r in plog.rows if r.get("kind") in ("grip_close", "grip_open") and r.get("sent") is True]
    new_grip = int(grip_rows[-1]["grip_cmd"]) if grip_rows else grip
    qa = arm.read()
    res = {"type": "correction", "action": "accept", "device": "gamepad", "correction_id": corr_id,
           "after_chunk": chunk, "t_start": t_start, "t_end": t_end, "duration_s": round(t_end - t_start, 2),
           "start_pose": _fmt6(start_q), "final_target": _fmt6(sent[-1]) if sent else _fmt6(start_q),
           "measured_after": _fmt6(qa), "steps_sent": len(sent), "pad_rows": len(plog.rows),
           "grip_before": grip, "grip_after": new_grip, "grip_used": bool(grip_rows), "regrip_used": False,
           "sent_targets": [_fmt6(x) for x in sent], "pad_end_reason": end_reason,
           "pad_stopped": bool(tp.stopped), "pad_stop_reason": tp.stop_reason, "pad_worker_done": bool(worker_done),
           "pad_ignored": sum(1 for r in plog.rows if r.get("accepted") is False)}
    print(f"[PAD   ] 補正 {corr_id} を終えました ({t_end - t_start:.1f} 秒, 送った目標 {len(sent)} 回"
          + (", 爪を動かした" if grip_rows else "") + "). この姿勢から推論に戻ります")
    if qa is not None:
        out = [k for k in range(6) if not (lo[k] - 0.5 <= qa[k] <= hi[k] + 0.5)]
        if out:
            res["outside_envelope"] = out
            print("[PAD   ] [注意] 学習データの範囲の外にいます: "
                  + ", ".join(f"j{k}={qa[k]:+.1f} (範囲 {lo[k]:+.0f}〜{hi[k]:+.0f})" for k in out)
                  + ". 次の推論は範囲の端へ切られるか, 止まります")
    if end_reason == "ctrl_c":
        res["ctrl_c"] = True
    return res, new_grip


def write_trial_json(run_dir, run_stem, args, recorded, end_reason, outcome, history_log, n_rewinds,
                     n_corrections, exit_code):
    """{run_stem}.json. お手本の試行 JSON と同じキー (recording_enabled / result / judgments) を持つ.
    結果 y なら pick / place とも human_verdict=yes (build_demo_episodes の採否と同じ読み方)."""
    verdict = {"y": "yes", "n": "no"}.get(outcome)
    result = ("completed" if end_reason in ("h", "q", "max_chunks") else
              "interrupted" if end_reason == "ctrl_c" else "aborted")
    j = {"script": "run_policy_client.py", "source": "pi05_policy", "run_stem": run_stem,
         "run_dir": str(run_dir), "recording_enabled": bool(recorded), "end_reason": end_reason,
         "result": result, "exit_code": exit_code,
         "outcome": {"y": "success", "n": "failure"}.get(outcome),
         "rewinds": n_rewinds, "corrections": n_corrections, "history_left": history_log,
         "args": {k: (str(v) if isinstance(v, Path) else v) for k, v in vars(args).items()},
         "ended_at": datetime.now().isoformat()}
    if verdict is not None:
        j["judgments"] = {"pick": {"human_verdict": verdict, "verdict": verdict, "source": "human"},
                          "place": {"human_verdict": verdict, "verdict": verdict, "source": "human"}}
    (Path(run_dir) / f"{run_stem}.json").write_text(json.dumps(j, indent=1, ensure_ascii=False))


def flush_stdin():
    """端末の入力にたまっているキーを捨てる (依頼 40: 位置合わせの画面を閉じたキーや, 送っている間に
    押したキーで確認が先へ進まないように). 端末でないとき (テストでパイプから流すとき) は何もしない."""
    try:
        tty = sys.stdin.isatty()
    except Exception:
        return
    if not tty:
        return
    try:
        import termios
        termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
    except Exception as ex:
        # 捨てられなかったことが分かるように残す (入力が勝手に進むのがまた起きたときに追えるように. codex 依頼 40)
        print(f"       [WARN] たまった入力を捨てられませんでした ({ex})")


CONFIRM_PROMPT = ("       Enter=実行 / s=送らず次へ / b=巻き戻し / p=ゲームパッドで補正 / c=キーで補正 / "
                  "r=爪の締め直し / h=HOMEに戻して終了 / q=終了 > ")
CONFIRM_KEYS = ("s", "b", "c", "p", "r", "h", "q")


def ask_outcome():
    while True:
        flush_stdin()
        raw = input("       結果: y=成功 / n=失敗 / Enter=記録しない > ")
        if raw == "":
            return None                  # 何も打たずに Enter だけ「記録しない」(空白だけは聞き直す. 依頼 40)
        a = raw.strip().lower()
        if a in ("y", "n"):
            return a
        print(f"       分からない入力: {raw}（y=成功 / n=失敗 / Enter=記録しない）")


def notify_recorder(sock, payload):
    try:
        payload = dict(payload, ts=time.time())
        sock.sendto((json.dumps(payload) + "\n").encode(), RECORDER)
    except Exception:
        pass


def apply_wait_args(args):
    """--send-wait などを この中の待ち (SEND_WAIT / GRIPPER_SLEEP / GRIP_OPEN_DAEMON_WAIT / SETTLE_WAIT_SEC)
    へ入れる. 指定がなければ今の値のまま. 決まった値は args にも書き戻す ({run_stem}.json の args に残る)."""
    global SEND_WAIT, GRIPPER_SLEEP, GRIP_OPEN_DAEMON_WAIT, SETTLE_WAIT_SEC
    for name, attr in (("send_wait", "SEND_WAIT"), ("grip_wait", "GRIPPER_SLEEP"),
                       ("grip_open_wait", "GRIP_OPEN_DAEMON_WAIT"), ("settle_wait", "SETTLE_WAIT_SEC")):
        v = getattr(args, name, None)
        if v is None:
            setattr(args, name, globals()[attr])
            continue
        if not np.isfinite(v) or v < 0:
            sys.exit(f"[ERROR] --{name.replace('_', '-')} は 0 以上の数で指定する (指定: {v})")
        globals()[attr] = float(v)
    print(f"[WAIT  ] 送ったあと {SEND_WAIT:.1f}s / 爪のあと {GRIPPER_SLEEP:.1f}s (開くときの daemon "
          f"{GRIP_OPEN_DAEMON_WAIT:.1f}s) / 止まるのを待つ目安 {SETTLE_WAIT_SEC:.1f}s")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--host", default="localhost")
    ap.add_argument("--port", type=int, default=8000)
    ap.add_argument("--execute", action="store_true", help="アームに送る (未指定はドライラン)")
    ap.add_argument("--auto", action="store_true", help="Enter 確認なしで連続実行")
    ap.add_argument("--steps-per-chunk", type=int, default=5, help="1 チャンクで何ステップ先まで進むか (0.1 秒単位)")
    ap.add_argument("--max-chunks", type=int, default=60)
    ap.add_argument("--max-move-deg", type=float, default=20.0, help="1 回の移動で各関節が動ける上限")
    ap.add_argument("--raw-dir", required=True,
                    help="安全範囲の算出に使う学習エピソード (必須. 学習に使った raw_episodes を指定する)")
    ap.add_argument("--prompt", default=TASK)
    ap.add_argument("--model", default=None,
                    help="サーバーが使っているモデルの名前 (記録に残すだけ. lab_start.sh が --model に合わせて渡す. 依頼 52)")
    ap.add_argument("--record", action="store_true", help="recorder.py --top に上カメラ動画を記録させる")
    ap.add_argument("--ignore-framing", action="store_true",
                    help="上カメラの写り方がお手本と違っても続ける (既定は何も送らずに終了)")
    ap.add_argument("--state-from-target", action="store_true",
                    help="モデルに渡す state の 6 関節を, 実測ではなく直前に送った目標にする "
                         "(既定 off = 実測). 肩の不足がチャンクごとに積み重なるのを防ぐ")
    ap.add_argument("--via-stride", type=int, default=0,
                    help="チャンクの途中の姿勢も中継点として順に送る (0 始まりで N-1, 2N-1, ... の "
                         "ステップ. 既定 0 = 従来どおり選んだ 1 点だけ送る)")
    ap.add_argument("--settle-tol", type=float, default=1.0,
                    help="送ったあとの残りのずれ (動きが止まってから測る) がこれを超えていたら "
                         "同じ目標を送り直す (度)")
    ap.add_argument("--settle-retries", type=int, default=2,
                    help="settle で同じ目標を送り直す回数の上限 (送信の試行回数). "
                         "0 で送り直さない = 従来の動き")
    ap.add_argument("--dry-keys", action="store_true",
                    help="ドライランでも 1 回ごとに止まり, b (巻き戻し) / c (補正) の流れを試す. "
                         "アームにも daemon にも触らず, HOME から始まる仮の姿勢を動かす")
    # 待ち時間 (依頼 35). 既定 None = この中の値 (縮めた値). 前の値に戻すときに指定する
    ap.add_argument("--send-wait", type=float, default=None,
                    help=f"送ったあと daemon が待つ秒 (既定 {SEND_WAIT:.1f}. 前は {SEND_WAIT_SEC:.1f}). "
                         "0 より大きいと Mac の応答待ちの上限が この値 + 3 秒になる")
    ap.add_argument("--grip-wait", type=float, default=None,
                    help=f"爪の開け閉めのあと Mac で待つ秒 (既定 {GRIPPER_SLEEP:.1f}. 前は 2.0)")
    ap.add_argument("--grip-open-wait", type=float, default=None,
                    help=f"爪を開くとき daemon が待つ秒 (既定 {GRIP_OPEN_DAEMON_WAIT:.1f}. 前は 2.0)")
    ap.add_argument("--settle-wait", type=float, default=None,
                    help=f"動きが止まるのを待つ目安の秒 (補正の 1 刻み・settle. 既定 {SETTLE_WAIT_SEC:.1f}. 前は 2.0)")
    args = ap.parse_args()
    if args.dry_keys and (args.execute or args.auto):
        sys.exit("[ERROR] --dry-keys はドライラン用 (--execute / --auto と一緒には使えない)")
    apply_wait_args(args)

    if not DATA_ROOT.exists():
        sys.exit(f"[ERROR] SSD 未マウント: {DATA_ROOT}")
    lo, hi, n_eps = load_envelope(Path(args.raw_dir))
    print(f"[SAFE  ] 安全範囲 (学習 {n_eps} 本 ±{ENVELOPE_MARGIN_DEG:.0f}°): "
          + "  ".join(f"j{k}[{lo[k]:+.0f},{hi[k]:+.0f}]" for k in range(6)))
    print(f"[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX={J1_SAFE_MAX:.0f}° で切った "
          f"(これを超える指令はアームが黙って捨てる / LOG352)  元データ: {args.raw_dir}")

    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_dir = LOG_ROOT / f"pi05_{stamp}"
    (log_dir / "frames").mkdir(parents=True, exist_ok=True)
    log_f = (log_dir / "log.jsonl").open("w")
    run_stem = f"pi05_{stamp}"
    mode = "EXECUTE" if args.execute else "DRY-RUN (アームには送らない)"
    print(f"[MODE  ] {mode}  steps/chunk={args.steps_per_chunk}  max_move={args.max_move_deg}°  log={log_dir}")
    print(f"[MODEL ] {args.model or '(指定なし)'}")
    # どのモデルで推論したかを log.jsonl の頭に残す ({run_stem}.json には args.model で残る. 依頼 52)
    log_f.write(json.dumps({"type": "model", "model": args.model, "raw_dir": str(args.raw_dir),
                            "host": args.host, "port": args.port, "t": time.time()},
                           ensure_ascii=False) + "\n")
    log_f.flush()

    # 以降は try/finally の中だけで資源を触る (codex R3: 途中で終了しても cap / log_f を必ず閉じる).
    # sys.exit は SystemExit を投げるので, この finally を通って後始末される.
    cap = policy = rec_sock = None
    exit_code = 0
    grip = 0
    grip_opened = False          # 開始時のグリッパー開放を済ませたか (codex R2)
    last_sent_target = None      # 直前のチャンクで最後に送った目標 (--state-from-target 用)
    recorder = None              # --execute のときの 1 秒 10 コマの記録 (policy_recorder.py)
    arm = None                   # 巻き戻し・補正の送り口 (ArmIO)
    history = []                 # 巻き戻せる区間 (推論 1 回分 / 補正 1 回分). 新しいものが後ろ
    n_rewinds = n_corrections = 0
    end_reason, outcome = None, None
    try:
        cams = camera_resolver.resolve_cameras(require_top=True, require_side=False, verbose=False)
        cap = cv2.VideoCapture(cams["top"])
        if not cap.isOpened():
            sys.exit("[ERROR] 上カメラを開けない")
        t_cam = time.time()
        while time.time() - t_cam < 8.0:       # 連係カメラは映像が来るまで数秒かかることがある
            ok, _f = cap.read()
            if ok and _f is not None:
                break
            time.sleep(0.2)
        else:
            sys.exit("[ERROR] 上カメラから映像が来ない. iPhone のロック解除・連係カメラの接続を確認 "
                     "(Photo Booth の『カメラ』メニューに iPhone が出るか)")

        # E (2026-10-21): 上カメラの写り方は 2 通りあり, カメラを開くたびに切り替わることがある.
        # お手本と違う写り方のまま動かすと, 学習と別の画像を見せることになる
        # (082645 は dy=+37.5 のまま実機を動かした. その 1 分前のドライラン 082512 は dy=-7.8).
        # アームは HOME に置いた状態で, 1 枚目を お手本の 1 枚目 (同じく HOME) と比べる.
        # **この確認が通るまで daemon にも触らない** (アームには何も送らない).
        ref_bgr, ref_path = load_reference(Path(args.raw_dir))
        bgr0 = None
        for _ in range(10):
            _rgb0, bgr0 = grab_rgb(cap)
            if bgr0 is not None:
                break
            time.sleep(0.2)
        if bgr0 is None:
            sys.exit("[ERROR] 上カメラの 1 枚目を撮れない")
        dx, dy, resp = measure_framing(ref_bgr, bgr0)
        verdict, why = framing_verdict(dx, dy, resp)
        print(f"[CAMERA] 写り方 dy={dy:+.1f} dx={dx:+.1f} response={resp:.3f} → {verdict} ({why})")
        print(f"[CAMERA] 基準: {ref_path}")
        log_f.write(json.dumps({"type": "framing", "dy": round(dy, 2), "dx": round(dx, 2),
                                "response": round(resp, 3), "verdict": verdict, "why": why,
                                "max_dy": FRAMING_MAX_DY, "max_dx": FRAMING_MAX_DX,
                                "min_response": FRAMING_MIN_RESPONSE,
                                "ok": verdict == "OK", "ignored": bool(args.ignore_framing),
                                "reference": str(ref_path)}) + "\n")
        log_f.flush()
        if verdict != "OK":
            if not args.ignore_framing:
                sys.exit(f"[ABORT ] 上カメラの写り方が『{verdict}』. アームには何も送らずに終了しました.\n"
                         f"         {why}\n"
                         "         pi05/camera_align.py で合わせ直す "
                         "(iPhone を付け直す / 連係カメラを開き直す).\n"
                         "         この写り方のまま続けるなら --ignore-framing")
            print(f"[WARN  ] 写り方が『{verdict}』だが --ignore-framing が指定されたので続けます")

        # ---- ここから先でアームに触る (写り方の確認を通ったあと) ----
        if not args.dry_keys:            # --dry-keys は daemon に触らない (アームなしで試す)
            get_daemon_socket()
        policy = PolicyClient(args.host, args.port)
        print(f"[POLICY] 接続 {policy.uri}  metadata={policy.metadata}")
        rec_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        if args.record:
            notify_recorder(rec_sock, {"type": "trial_start", "run_stem": run_stem})
        if args.execute:
            # 1 秒 10 コマの記録 (上カメラ + 実測角). ここから先, 上カメラは記録スレッドだけが読む
            install_daemon_lock()
            recorder = PolicyRecorder(log_dir, cap, read_angles_once, frame_to_model,
                                      angles_ok=wr._is_valid_angles)   # 本体が読んだ角度も同じ確かめで残す (依頼 46)
            recorder.start()
            print(f"[RECORD] 上カメラと実測角を 1 秒 10 コマで記録します → {log_dir / 'recording'}")
        arm = ArmIO(args.execute, recorder, sim_q=(HOME if args.dry_keys else None))
        if args.dry_keys:
            print("[DRY   ] --dry-keys: アームには送らず, HOME から始まる仮の姿勢で流れを試します")

        q = arm.read() if args.dry_keys else read_angles()
        if q is None:
            sys.exit("[ERROR] 関節角を読めない (daemon を確認)")
        if np.max(np.abs(q - np.array(HOME))) > 5.0:
            print(f"[WARN  ] HOME から離れています (max|Δ|={np.max(np.abs(q - np.array(HOME))):.1f}°). "
                  "学習データは HOME から始まっています")
            print("         現在角: " + " ".join(f"j{k}={v:+.1f}" for k, v in enumerate(q)))
        if args.execute:
            # 開始時の開放は, 最初に送る目標が検査を通ってからにする (codex R2).
            # 以前はここで開いていたので, 1 チャンク目が安全ガードで止まる場合でも
            # 爪だけ先に開いてしまっていた (物を持っていれば落とす).
            print("[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います "
                  "(爪が閉じていたら手で開けておく)")

        end_reason = "max_chunks"
        for i in range(args.max_chunks):
            q = arm.read() if args.dry_keys else read_angles()
            if q is None:
                print("[ERROR] 関節角を読めない (daemon を確認). 停止"); exit_code = 1; end_reason = "error"; break
            rgb = bgr_small = None
            for _ in range(10):
                rgb, bgr_small = grab_rgb(cap) if recorder is None else recorder.grab()
                if rgb is not None:
                    break
                time.sleep(0.2)
            if rgb is None:
                print("[ERROR] 上カメラの映像が途切れた (iPhone のスリープ等). 停止")
                exit_code = 1; break
            # モデルに渡す state の 6 関節 (--state-from-target のときだけ「直前に送った目標」).
            # apply_limits / check_send_safe / 中継点のかけ直しは今までどおり実測 q を使う.
            measured_state = np.concatenate([q, [float(grip)]]).astype(np.float32)
            state_joints, state_source = q, "measured"
            if args.state_from_target:
                if last_sent_target is None:
                    print("[STATE ] 実測を渡す (直前に送った目標がない)")
                else:
                    gap = float(np.max(np.abs(last_sent_target - q[:6])))
                    if gap > STATE_TARGET_MAX_GAP_DEG:
                        print(f"[WARN  ] 目標と実測の差が {gap:.2f}° > {STATE_TARGET_MAX_GAP_DEG:.0f}° "
                              "なので実測を渡します (腕がどこかに当たって止まっている可能性)")
                    else:
                        state_joints, state_source = last_sent_target, "target"
                    print(f"[STATE ] {'目標' if state_source == 'target' else '実測'}を渡す  "
                          f"目標と実測の最大差 {gap:.2f}°"
                          f"（関節 j{int(np.argmax(np.abs(last_sent_target - q[:6])))}）")
            state = np.concatenate([state_joints, [float(grip)]]).astype(np.float32)
            t0 = time.time()
            out = policy.infer({"observation/image": rgb, "observation/state": state, "prompt": args.prompt})
            latency = time.time() - t0
            acts = np.asarray(out["actions"], dtype=float)
            raw_target, grip_cmd, used = plan_chunk(acts, q, grip, args.steps_per_chunk)
            # plan_chunk は, グリッパーが切り替わるチャンクでは used-1 行目 (used=0 なら実測角)
            # を目標に返す. actions の何行目を使ったかを記録に残す (codex A1).
            final_raw_index = (used if grip_cmd is None else (used - 1 if used > 0 else None))
            # 中継点の計画 (--via-stride 0 なら used の 1 点だけ = 従来と同じ).
            plan = plan_vias(acts, q, raw_target, used, args.via_stride, lo, hi,
                             args.max_move_deg, final_raw_index)
            target, notes = plan[-1]["target"], plan[-1]["notes"]     # チャンクの最終目標
            delta = target - q

            cv2.imwrite(str(log_dir / "frames" / f"{i:04d}.jpg"), bgr_small)
            rec = {"chunk": i, "t": time.time(), "latency_s": round(latency, 3), "state": state.tolist(),
                   "state_source": state_source, "measured_state": measured_state.tolist(),
                   "actions": acts.tolist(), "used_step": used, "target": target.tolist(),
                   "grip_cmd": grip_cmd, "notes": notes, "executed": False}
            gtxt = "" if grip_cmd is None else ("  グリッパー→閉" if grip_cmd == 1 else "  グリッパー→開")
            print(f"[{i:03d}] 推論 {latency*1000:.0f}ms  {used + 1}ステップ先  Δ="
                  + " ".join(f"{d:+5.1f}" for d in delta) + gtxt)
            for n in notes:
                print(f"       [LIMIT] {n}")
            if args.via_stride > 0:
                rec["vias"] = [{"step": v["step"], "raw_index": v["raw_index"],
                                "raw": _fmt6(v["raw"]), "planned_target": _fmt6(v["target"]),
                                "target": _fmt6(v["target"]), "notes": v["notes"],
                                "called": False, "success": None, "retried": None,
                                "actual_after": None} for v in plan]
                print(f"       [VIA   ] 中継点 {len(plan)} 点: step "
                      + ", ".join(str(v["step"]) for v in plan))
                for v in plan:
                    for n in v["notes"]:
                        print(f"       [LIMIT] step {v['step']}: {n}")
                    if not args.execute:   # ドライランは中継点の一覧だけ出して送らない
                        print(f"       [VIA   ] step {v['step']:2d}  Δ="
                              + " ".join(f"{d:+5.1f}" for d in v["delta"]))

            # 送る直前の最終検査 (codex R1 / C2). 中継点は 1 点でも引っかかったら
            # **どれも送らずに** 止める (途中まで動かして止まる方が危ない).
            # 送信にもグリッパー操作にも入らず止める.
            # HOME 復帰 (h) だけはこの検査を通さずに送れる (範囲の外にいるアームを戻す手段).
            unsafe = []
            for v in plan:
                for u in check_send_safe(v["target"], lo, hi):
                    unsafe.append(u if args.via_stride <= 0 else f"step {v['step']}: {u}")

            stopped = False
            if not unsafe and (args.execute or args.dry_keys):
                # ここから先で送れなかった経路 (s / 送信なし / 中断) は, 次のチャンクで
                # 実測を渡すために None のままにする (--state-from-target 用).
                last_sent_target = None
                grasp = None         # つかんだ区間を巻き戻すときの答え (GRASP_PLACED / GRASP_DROPPED)
                if not args.auto:
                    # 入力を待っている間は wait (人が手でブロックを置き直す. 学習データに使わない. 依頼 27)
                    if recorder is not None:
                        recorder.set_mode(MODE_WAIT)
                    while True:
                        grasp = None
                        # 依頼 40: たまっているキー (位置合わせを閉じたキー・押しっぱなし) で先へ進まない
                        flush_stdin()
                        raw = input(CONFIRM_PROMPT)
                        ans = raw.strip()
                        if raw == "":
                            break                # 実行するのは何も打たずに Enter を押したときだけ (依頼 40)
                        if ans not in CONFIRM_KEYS:
                            # 知らない文字 (R・c] ・空白だけ など) は実行せずに聞き直す. 先頭の文字だけでは見ない
                            print(f"       分からない入力: {raw}（{CONFIRM_PROMPT.strip().rstrip('>').strip()}）")
                            continue
                        if ans == "r":
                            # 依頼 40: 補正の外でも r で爪を締め直す (補正の中の r と同じ. チャンクは実行しない)
                            e = {"type": "confirm", "action": "regrip_tighten", "where": "confirm",
                                 "correction_id": None, "after_chunk": i, "chunk": i, "t": time.time(),
                                 "key": "r", "before": grip}
                            if grip != 1:
                                print("       爪が開いているので締め直しはしない（閉じるのは g）")
                                e.update(after=grip, sent=False, skipped="gripper_open")
                            else:
                                print("       爪を締め直します (開かずに閉じる指令をもう一度)")
                                arm.gripper(1)
                                e.update(after=1, sent=True)
                            log_f.write(json.dumps(e) + "\n"); log_f.flush()
                            continue
                        if ans != "b":
                            break
                        # 巻き戻せない場合は断って聞き直す (アームは動かさない)
                        if not history:
                            print("       [REWIND] 巻き戻せる区間がありません (まだ何も実行していない)")
                            continue
                        if history[-1].get("blocked"):
                            print("       [REWIND] 直前の巻き戻し / 補正の取り消しが始めの姿勢まで戻りきれなかったので, "
                                  "これより前へは巻き戻しません (h で HOME に戻せます)")
                            continue
                        h_last = history[-1]
                        if (h_last["kind"] == "correction" and h_last.get("regrip")
                                and not h_last["grip_changed"]):
                            # R を使った補正 (依頼 35): 始めも終わりも閉. つかんだ区間と同じく 1 文字で聞く
                            a = input(f"       [REWIND] 補正 {h_last['correction_id']} では R でつかみ直しました. "
                                      "y=持っている（閉じたまま戻る） / "
                                      "n=落とした（先に開いてから戻る） / その他=やめる > ").strip().lower()
                            if a == "y":
                                grasp = GRASP_HELD
                            elif a == "n":
                                grasp = GRASP_DROPPED
                            else:
                                print("       [REWIND] やめました (アームは動かしていません)")
                                continue
                            break
                        if h_last["grip_changed"]:
                            if h_last["kind"] == "chunk" and h_last.get("grip_cmd") == 1:
                                # つかんだ区間 (依頼 27): 持っているか落としたかを 1 文字で聞く
                                a = input(f"       [REWIND] 推論 {h_last['chunk']} はつかんだ区間です. "
                                          "y=持っている（下ろして置いてから戻る） / "
                                          "n=落とした（先に開いてから戻る） / その他=やめる > ").strip().lower()
                                if a == "y":
                                    grasp = GRASP_PLACED
                                elif a == "n":
                                    grasp = GRASP_DROPPED
                                else:
                                    print("       [REWIND] やめました (アームは動かしていません)")
                                    continue
                                break
                            if h_last["kind"] == "chunk" and h_last.get("grip_cmd") == 0:
                                print(f"       [REWIND] 直前の区間 (推論 {h_last['chunk']}) でグリッパーを開いた"
                                      "ので巻き戻しません. ブロックを置き直すなら h で終えてやり直してください")
                                continue
                            print(f"       [REWIND] 直前の区間 ({'推論 ' + str(h_last['chunk']) if h_last['kind'] == 'chunk' else '補正 ' + str(h_last['correction_id'])})"
                                  " でグリッパーを動かしたので巻き戻しません")
                            continue
                        break
                    if recorder is not None and ans not in ("q", "h"):
                        recorder.set_mode(MODE_MODEL)
                    if ans == "q":
                        end_reason = "q"
                        log_f.write(json.dumps(rec) + "\n"); log_f.flush()
                        if recorder is not None:
                            recorder.stop()
                            outcome = ask_outcome()
                        break
                    if ans == "h":
                        end_reason = "h"
                        if recorder is not None:
                            recorder.set_mode(MODE_HOME)
                        if args.dry_keys:
                            print("       [DRY   ] HOME へは送らない (ドライラン)")
                        else:
                            ok_home, _ = _send_and_wait_arrival(HOME, "pi05_home", 2.5, send_wait_sec=SEND_WAIT)
                            rec["home_requested"] = True
                            rec["home_arrived"] = bool(ok_home)
                        log_f.write(json.dumps(rec) + "\n"); log_f.flush()
                        if recorder is not None:
                            recorder.stop()
                            outcome = ask_outcome()
                        break
                    if ans == "s":
                        log_f.write(json.dumps(rec) + "\n"); continue
                    if ans == "b":
                        # 直前の 1 回分を巻き戻して, 戻った姿勢からもう一度推論する
                        rec["rewind_requested"] = True
                        log_f.write(json.dumps(rec) + "\n"); log_f.flush()
                        entry = history.pop()
                        rw = run_rewind(entry, arm, lo, hi, args.max_move_deg, recorder, grasp=grasp)
                        rw["after_chunk"] = i
                        n_rewinds += 1
                        if rw.get("gripper_opened"):
                            grip = 0
                        if grasp == GRASP_DROPPED and entry["kind"] == "chunk":
                            rw["start_frame"] = str(log_dir / "frames" / f"{entry['chunk']:04d}.jpg")
                        if not rw["finished"]:
                            # codex R2-2: 戻りきれなかったら境界を積む (次の b で古い区間の経路へ飛ばない)
                            history.append({"kind": "blocked", "blocked": True, "chunk": entry.get("chunk"),
                                            "correction_id": entry.get("correction_id"),
                                            "grip_changed": False, "t_start": rw["t_start"],
                                            "t_end": rw["t_end"]})
                        log_f.write(json.dumps(rw) + "\n"); log_f.flush()
                        if grasp == GRASP_DROPPED and rw.get("gripper_opened"):
                            # 落としたブロックを人が元の位置へ置き直す間は wait (手が写る)
                            if "start_frame" in rw:
                                print(f"       [REWIND] 元の位置の目安 (推論 {entry['chunk']} の始めの写真): "
                                      f"{rw['start_frame']}")
                            if recorder is not None:
                                recorder.set_mode(MODE_WAIT)
                            input("       ブロックを元の位置へ置き直したら Enter で続ける > ")
                            if recorder is not None:
                                recorder.set_mode(MODE_MODEL)
                        continue
                    if ans in ("c", "p"):
                        rec["correction_requested"] = True
                        rec["correction_device"] = "gamepad" if ans == "p" else "keys"
                        log_f.write(json.dumps(rec) + "\n"); log_f.flush()
                        n_corrections += 1
                        if ans == "p":
                            cr, grip = run_pad_correction(arm, grip, n_corrections, i, log_f, recorder, lo, hi)
                        else:
                            with KeyReader() as keys:
                                cr, grip = run_correction(arm, keys, lo, hi, args.max_move_deg, grip,
                                                          n_corrections, i, log_f, recorder)
                        if cr is not None:
                            log_f.write(json.dumps(cr) + "\n"); log_f.flush()
                            if cr["grip_used"] or cr.get("regrip_used"):
                                # codex R2-1: 補正でグリッパーを動かしたら, 開始時の開放は済んだことにする
                                # (このあと最初の推論の実行で勝手に開いて物を落とさない. R も同じ)
                                grip_opened = True
                            if cr["action"] == "cancel" and not cr["finished"]:
                                history.append({"kind": "blocked", "blocked": True, "chunk": None,
                                                "correction_id": n_corrections, "grip_changed": False,
                                                "t_start": cr["t_start"], "t_end": cr["t_end"]})
                            if cr["action"] == "accept" and (cr["steps_sent"] or cr["grip_used"]
                                                             or cr["regrip_used"]):
                                history.append({"kind": "correction", "correction_id": n_corrections,
                                                "chunk": None,
                                                "start_pose": np.asarray(cr["start_pose"], dtype=float),
                                                "sent_targets": [np.asarray(x, dtype=float)
                                                                 for x in cr["sent_targets"]],
                                                "grip_changed": cr["grip_used"],
                                                "regrip": cr["regrip_used"],
                                                "t_start": cr["t_start"], "t_end": cr["t_end"]})
                            if cr.get("ctrl_c"):
                                raise KeyboardInterrupt
                        continue
            if not unsafe and args.dry_keys:
                # --dry-keys の Enter: 送らずに仮の姿勢を目標へ移す (b / c の流れを試すため)
                t_ex0 = time.time()
                sent = [np.asarray(v["target"], dtype=float).copy() for v in plan]
                arm.send(sent[-1], f"pi05_chunk{i:03d}")
                if grip_cmd is not None:
                    arm.gripper(grip_cmd)
                    grip = grip_cmd
                history.append({"kind": "chunk", "chunk": i, "correction_id": None,
                                "start_pose": q[:6].copy(), "sent_targets": sent,
                                "grip_changed": grip_cmd is not None, "grip_cmd": grip_cmd,
                                "t_start": t_ex0, "t_end": time.time()})
                rec["simulated"] = True
            if not unsafe and args.execute:
                t_exec_start = time.time()
                sent_targets = []        # 実際に送った目標 (送った順. 巻き戻しでこれを逆にたどる)
                if not grip_opened:
                    # 開始時の開放はここで 1 度だけ (写り方の確認と目標の検査を通り, 送ると決めてから)
                    print("[GRIP  ] 開始時にグリッパーを開く")
                    if recorder is not None:
                        recorder.gripper(0)
                    gripper_cmd(0)
                    grip_opened = True
                    rec["initial_gripper_open"] = True
                broke, final_target, final_sent, sent_any = False, None, False, False
                for vi, v in enumerate(plan):
                    v_last = vi == len(plan) - 1
                    tag = (f"pi05_chunk{i:03d}" if args.via_stride <= 0
                           else f"pi05_chunk{i:03d}_via{v['step']:02d}")
                    tgt, vdelta = v["target"], v["delta"]
                    if args.via_stride > 0:
                        # 送る直前に実測角を読み, そこから apply_limits と check_send_safe を
                        # かけ直す (codex A2: 1 点だけ送る従来の方式と同じ基準にそろえる).
                        qa = read_angles()
                        if qa is None:
                            print("       [WARN] 送る直前の関節角を読めない. "
                                  "この中継点も残りの中継点も送りません")
                            print("       [WARN] 未到達. j4 の発熱などを確認してください")
                            rec["arrived"] = False
                            broke = True
                            if args.auto:
                                exit_code = 1
                                log_f.write(json.dumps(rec) + "\n")
                                stopped = True
                            break
                        tgt, notes_now = apply_limits(v["raw"], qa, lo, hi, args.max_move_deg)
                        if notes_now != v["notes"]:
                            for n in notes_now:
                                print(f"       [LIMIT] step {v['step']} (実測から): {n}")
                        rec["vias"][vi].update(target=_fmt6(tgt), notes=notes_now)
                        u2 = check_send_safe(tgt, lo, hi)
                        if u2:
                            unsafe = [f"step {v['step']}: {x}" for x in u2]
                            broke = True
                            break
                        vdelta = tgt - qa          # 送らない判定は実測との差で見る (codex ❌1)
                    final_target = tgt
                    if np.max(np.abs(vdelta)) <= 0.5:
                        if args.via_stride > 0:
                            print(f"       [VIA   ] step {v['step']:2d}  Δ="
                                  + " ".join(f"{d:+5.1f}" for d in vdelta) + "  送らず (0.5° 以下)")
                        continue
                    ok, retried = _send_and_wait_arrival(tgt.tolist(), tag, 3.0, send_wait_sec=SEND_WAIT)
                    sent_any = True
                    sent_targets.append(np.asarray(tgt, dtype=float).copy())
                    if args.via_stride > 0:
                        rec["vias"][vi].update(called=True, success=bool(ok), retried=bool(retried),
                                               actual_after=_fmt6(read_angles()))
                        print(f"       [VIA   ] step {v['step']:2d}  Δ="
                              + " ".join(f"{d:+5.1f}" for d in vdelta)
                              + ("  着いた" if ok else "  未到達"))
                    if v_last:
                        rec["arrived"] = bool(ok)
                        final_sent = True
                    if not ok:
                        print("       [WARN] 未到達. j4 の発熱などを確認してください")
                        if args.auto:
                            exit_code = 1
                            log_f.write(json.dumps(rec) + "\n")
                            stopped = True
                            broke = True
                            break
                    if v_last:
                        # 残ったずれを同じ目標の送り直しで詰める (LOG359). 止める条件は増やさない.
                        rec["settle"], rec["settle_retries_used"] = settle_target(
                            tgt, f"pi05_chunk{i:03d}", args.settle_tol, args.settle_retries)
                if not broke and args.via_stride > 0 and not final_sent and final_target is not None:
                    # 最後の中継点が「送らない」になった場合も, チャンクの最後の目標について
                    # 到達の記録と settle は必ず行う (codex ❌2: 重複送信を省くことと分ける).
                    qa = read_angles()
                    rec["arrived"] = bool(qa is not None
                                          and float(np.max(np.abs(final_target - qa[:6]))) <= 3.0)
                    rec["settle"], rec["settle_retries_used"] = settle_target(
                        final_target, f"pi05_chunk{i:03d}", args.settle_tol, args.settle_retries)
                if not broke:
                    # 次のチャンクで --state-from-target が使う「直前に送った目標」.
                    # 送らなかった (s / 送信なし / 中断) ときは None に戻して実測を渡す.
                    last_sent_target = (np.asarray(final_target, dtype=float).copy()
                                        if sent_any and final_target is not None else None)
                    if grip_cmd is not None:
                        if recorder is not None:
                            recorder.gripper(grip_cmd)
                        gripper_cmd(grip_cmd)
                        grip = grip_cmd
                    rec["executed"] = True
                # 巻き戻し (b) 用に, この推論で実際に送った目標と始めの姿勢 (実測) を残す.
                # 途中で止まった (broke) 推論も, 1 点でも送っていれば残す (codex R1-3:
                # 残さないと次の b が 1 つ古い区間を巻き戻してしまう)
                t_exec_end = time.time()
                rec["t_exec_start"], rec["t_exec_end"] = t_exec_start, t_exec_end
                rec["sent_targets"] = [_fmt6(x) for x in sent_targets]
                grip_moved = (not broke) and grip_cmd is not None
                if sent_targets or grip_moved:
                    history.append({"kind": "chunk", "chunk": i, "correction_id": None,
                                    "start_pose": q[:6].copy(), "sent_targets": sent_targets,
                                    "grip_changed": grip_moved, "grip_cmd": grip_cmd if grip_moved else None,
                                    "partial": bool(broke),
                                    "t_start": t_exec_start, "t_end": t_exec_end})
            if stopped:
                break
            if unsafe:
                # 計画の検査 または 送る直前のかけ直しで範囲の外になったとき.
                # 送信にもグリッパー操作にも入らず止める.
                # HOME 復帰 (h) だけはこの検査を通さずに送れる (範囲の外にいるアームを戻す手段).
                rec["unsafe"] = unsafe
                rec["envelope"] = {"lo": [round(float(v), 2) for v in lo],
                                   "hi": [round(float(v), 2) for v in hi]}
                for u in unsafe:
                    print(f"       [ABORT] {u}")
                print("[STOP  ] 安全でない目標のため, 送らずに停止します "
                      "(現在角が安全範囲の外にあると, 移動量の制限で範囲の外に戻る)")
                # 記録は入力より前に書いて flush する (codex C2-1: 入力待ちや HOME 復帰中の
                # Ctrl-C でこのチャンクの unsafe / envelope が消えないように).
                log_f.write(json.dumps(rec) + "\n"); log_f.flush()
                exit_code = 1
                if args.execute and not args.auto:
                    if recorder is not None:
                        recorder.set_mode(MODE_WAIT)      # 入力待ちは wait (codex 依頼 27 P3)
                    ans = input("       h=HOMEに戻して終了 / 他=そのまま終了 > ").strip().lower()
                    if ans == "h":
                        if recorder is not None:
                            recorder.set_mode(MODE_HOME)
                        ok_home, _ = _send_and_wait_arrival(HOME, "pi05_home", 2.5, send_wait_sec=SEND_WAIT)
                        log_f.write(json.dumps({"chunk": i, "type": "home_return", "t": time.time(),
                                                "home_requested": True,
                                                "home_arrived": bool(ok_home)}) + "\n")
                        log_f.flush()
                else:
                    print("         アームを HOME に置き直してからやり直してください")
                break
            log_f.write(json.dumps(rec) + "\n"); log_f.flush()
    except KeyboardInterrupt:
        print("\n[STOP  ] Ctrl-C")
        end_reason = "ctrl_c"
    finally:
        # finally ではアームを動かさない (HOME 復帰もサーボ解放もしない). 止まった姿勢は
        # そのまま保つ: 未知の姿勢から勝手に動かす方が危ない. 戻すときは h で明示的に送る.
        if recorder is not None:
            recorder.stop()
            try:
                write_trial_json(log_dir, run_stem, args, bool(recorder.stopped_cleanly),
                                 end_reason if not (exit_code and end_reason in (None, "max_chunks"))
                                 else "error",
                                 outcome, [{k: v for k, v in h_.items()
                                            if k in ("kind", "chunk", "correction_id", "grip_changed")}
                                           for h_ in history], n_rewinds, n_corrections, exit_code)
            except Exception as e:
                print(f"[WARN  ] {run_stem}.json を書けない: {e}")
        if args.record and rec_sock is not None:
            notify_recorder(rec_sock, {"type": "trial_end", "run_stem": run_stem})
        if not log_f.closed:
            log_f.close()
        if cap is not None and not (recorder is not None and recorder.alive()):
            cap.release()      # 記録スレッドが止まらなかったときは閉じない (読みかけと競合する)
        if policy is not None:
            policy.close()
        try:
            close_daemon_socket()
        except Exception:
            pass
        print(f"[DONE  ] 記録: {log_dir}")
    # 終了コードは正常に抜けた経路でだけ表示する (sys.exit で抜ける場合は, その [ABORT] /
    # [ERROR] の文言が出て終了コード 1 になるので, ここで 0 と書くと食い違う).
    if exit_code:
        print(f"[DONE  ] 終了コード {exit_code} (安全停止 / 途中のエラー)")
    return exit_code


if __name__ == "__main__":
    sys.exit(main())
```

### tools/gamepad_teleop.py（commit cd2fb46）

#### tools/gamepad_teleop.py

```python
#!/usr/bin/env python3
"""gamepad_teleop.py — ゲームパッドで mechArm 270 を手で動かす (依頼 68. 2026-11-02)

PICK で爪の位置がずれたときに, エイイチがゲームパッドで左右・上げ下げ・爪の開閉を直す.
押した分だけそのまま動く. 自動で動く要素は入れない (自動の握り直し・失敗時の自動 HOME などはない).
押した操作と, 送った角度・届いた角度をすべて log.jsonl に残す (あとで「人が直した記録」として使う).

操作 (エイイチが決めた. 変えない):
  十字キー 左 / 右 (13 / 14)  j0 を 1° ずつ (2026-11-03 に 0.5° から変えた. 0.5° ではサーボが反応しなかった). どの関節を選んでいても いつも j0
                              (向き: 左 = J0_LEFT_SIGN. 依頼に向きの指定がないので, 右 = +, 左 = − にしてある)
  十字キー 上 / 下 (11 / 12)  選んでいる関節を 1° ずつ. 上 = −, 下 = +. 始めは j1
  十字キーの長押し            押している間, 止めずに動かし続ける. 短く押せば 1 刻み.
                              (2026-11-05: 押した瞬間には送らない. 0.4 秒より前に離せば 1 刻み, 押し続ければ長押しの
                              どちらか片方だけを送る. 動いている間に押した十字キーは, 最後の 1 つだけ 終わってから送る)
                              (2026-11-05 作り直し. 3° 先を 0.1 秒おきに送り直す形はカクついた. jog_probe の 3 回の実機で,
                              送り直すたびに加速からやり直すこと, ゆっくりの速さではどの命令も 0.2 秒ごとに止まることが分かった)
                              押して HOLD_START_SEC (0.4 秒) たっても押していれば, 「いま読んだ角度から HOLD_AHEAD_DEG (15°) 先」を
                              speed 20 (毎秒約 40°) で 1 回だけ送り, アーム自身の加速・減速で動かす. HOLD_INTERVAL_SEC (0.1 秒) おきに
                              角度を読み, 目標まで残り HOLD_REFILL_DEG (5°) 以下になったら次の 15° 先を送る.
                              離したら daemon の stop (jog_stop と stop) を送って, その場で止める (行きすぎは実機で約 3°).
                              stop が届かなくても, アームは送ってある目標 (最大 15° 先) で止まる.
                              長押しの送信では届いたかを待たない.
                              R1 は短い押しの刻み (2°) にだけ効く. 長押しの速さは speed で決まる (20 のまま).
                              離したら, 送ると決める前の送信は取り消す.
                              押し始めに受け付けられなかった押し (動いている間・停止中) は, 押し続けても長押しにしない.
                              離した合図を取りこぼしても止まるよう, 毎コマ コントローラーのボタンの状態も見る.
                              HOLD_STALL_SEC (1 秒) のあいだ動かした関節が進んでいなければ, その長押しは止める (当たっているなど).
                              離す・L2+R2・範囲の端・△・ほかのボタン・接続が切れる で長押しは止まる
  △ (3)                       上下で動かす関節を切り替える: j1 → j2 → j3 → j4 → j5 → j1
  R1 (10) を押しながら        1 回 2° ずつ (十字キーの左右・上下とも)
  × (0) / ○ (1)              爪を閉じる / 開く
  HOME (5) を 1 秒長押し      HOME [0, 0, 0, 0, 0, -30] に戻る. 1 秒続けて押したときに 1 回だけ送る
  L2 と R2 (軸 4・5) の両方が 0.0 を超えたとき  停止. 以後の動く操作を受け付けない. OPTIONS (6) で再開
                              2026-11-05 から, 停止のときも daemon の stop を送り, 動いている途中の動き (HOME を含む) も止める
  ほかのボタン (□・SHARE・L1・L3・R3・タッチパッド・スティック) は何もしない

守り:
  - j1 は 90.87 (arm_kinematics.md の PICK_J1_FIXED) を超える指令を送らない
    (fk_lookup.J1_SAFE_MAX = 88.0 は別の用途の値なので, ここでは使わない)
  - 各関節は物理可動域 (wr.JOINT_LIMITS) まで. j4 は J4_SAFE (−90〜+60)
    (基準の姿勢から j0 ±15°・j1〜j5 ±10° の枠は 2026-11-03 にやめた. HOME から手でブロックの上まで運ぶため)
  - 範囲で切られたら理由を画面とログに出す. 別の関節で補わない
  - HOME だけは範囲の検査を通さずに送れる (run_policy_client.py の h と同じ扱い)

送り方:
  - 十字キー (1 刻み・長押し) は, 動かす関節だけを send_angle で送る (2026-11-05. ほかの関節には指令を送らない).
    HOME は send_angles で 6 関節. send_coords は使わない. speed 20 (j0_sweep_scan.SPEED)
  - 1 回押すごとに目標を 1 つ作って送り, 3° 以内に届いたかを読んで確かめる.
    送った指令が無視されることがある (どの関節も動かない. 2026-11-03 の実機で 3 回, 8 秒止まった.
    waypoint_replay.py の「単発は無視されるが, 打ち直せば通る」と同じ). 送ってから IGNORED_CHECK_SEC
    (0.5 秒) たっても, 届いておらず, どの関節も IGNORED_MOVE_DEG (0.3°) 動いていなければ, 同じ目標を
    送り直す (IGNORED_MAX_RESENDS 回まで). 送り直しも SendGate の中で行うので, 停止のあとは送らない
  - 送ってから届くまで (または待ちの上限まで) と, 爪のあとの GRIPPER_SLEEP の間は
    次の動く操作を受け付けない (ためない). 停止はいつでも受け付ける
    (送る仕事は別のスレッドで行い, 画面とコントローラーの読み取りは止めない)
  - 停止が入ったあとは新しい指令を送らない. 動いている途中の動きは daemon の stop で止める
    (2026-11-05 に daemon へ stop を足した. tools/daemon_add_stop.sh. stop のない古い daemon なら,
    今の角度へ send_angles して止める. ただしこれは実機で効かない回があった)

1 回の出発点 (2026-11-05 に関節ごとにした):
  関節ごとに, 直前の指令と送る前に読んだ角度が 3° 以内なら 指令, 離れていれば 読んだ角度 (start_pose).
  動かす関節は, 直前の指令に届いていなければ 読んだ角度.
  長押しを止めたあとは何も送らない. 止まった関節だけ止まった角度を指令にし, ほかの関節の指令は変えない
  (重さで下がった分を出発点にすると, 止めるたびにずれが積み上がった. 下がった j1 を指令へ送り直す形は,
   届かない指令のせいで次の指令が無視されやすくなったので 11/5 16:13 にやめた).
  動かさない関節は, 指令と読んだ角度が 10° 以内なら指令のまま (START_FALLBACK_DEG).
  1 刻みの「届いた」は, 動かした関節だけを見て 0.5° 以内 (STEP_REACH_TOL_DEG). 動かなければ 3 回まで送り直し, それでも動かなければ戻る.
  長押しで目標の手前で止まったら, 0.3 秒で同じ目標を送り直す (3 回まで).
  (以前の説明: 直前に送った目標に届いていて, 送る前に読んだ角度とも 3° 以内なら その目標. そうでなければ 読んだ角度.)
  (j1 の重さによる 2〜3° の不足があっても, 0.5° の刻みが読んだ値に引きずられないように.
   手で動かされたなど 3° を超えて離れていたら, 古い目標から大きく動かさない)
  出発点の 6 関節のどれかが範囲の外なら送らない (範囲の端へ切って 1 回で大きく動かさない).

停止と送信の順番 (codex R1):
  「送ると決める」と「停止・終了の取り消し」を 仕事ごとの鍵 (SendGate) で並べる (codex R1・R2).
  停止のあとに送ると決める指令はない. 停止は待たされない (画面も止まらない). 停止より前に決めた送信は
  通信の上限 SEND_RPC_TIMEOUT_SEC (8 秒) の中で終わる. 止める前に作った仕事は OPTIONS で再開しても生き返らない.
  1 コマの中では L2+R2 と読み取りの失敗を ボタンより先に見る. 送ったあと確かめる途中で関節角を
  読めなければ停止にする. log.jsonl に書けなくなったら停止し, 再開させない.

終わり方:
  Ctrl+C / ウィンドウを閉じる. 新しい指令は送らずに終わる. アームは動かさない
  (HOME 復帰もサーボ解放もしない. run_policy_client.py の finally と同じ)

Usage:
  python3 tools/gamepad_teleop.py              # ドライラン (daemon にもアームにも触らない. HOME から始まる仮の姿勢)
  python3 tools/gamepad_teleop.py --execute    # daemon に送る

記録 (--execute のときだけ): /Volumes/KIOXIA/r2r2r-data/runs/experiments/gamepad_teleop/{run_stem}/
      log.jsonl (押した操作 1 つにつき 1 行) / meta.json / gamepad_teleop.py (使ったスクリプトの写し)
      通しの完走ではないので台帳には書かない.
      ドライランは記録しない (KIOXIA に何も書かない・フォルダも作らない. KIOXIA がなくても動く. 依頼 69)

読み込み (依頼 69):
  run_policy_client・j0_sweep_scan・visual_servo_pick は cv2 を読み, cv2 は自分の libSDL2
  (site-packages/cv2/.dylibs/) を持っている. pygame の libSDL2 と 2 つ入ると objc の
  「Class SDLApplication is implemented in both ...」が十数行出る. なのでこの 3 つは --execute のときだけ
  load_daemon_modules() で読む (ドライランでは cv2 も daemon の送り口も読まない).
  ドライランでも要る値 (HOME・J4_SAFE・GRIPPER_SLEEP・物理可動域) は, 読み込まずに元のファイルの
  代入から読む (二重に書かない). --execute で読んだあと, 読み込んだ値と同じかを確かめる.
  --execute では cv2 を読むので 警告は今までどおり出る (動きには影響なし).
"""
import argparse
import ast
import hashlib
import json
import os
import platform
import queue
import shutil
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path

# ウィンドウが手前にないときもコントローラーの入力を受け取る (gamepad_check.py と同じ)
os.environ.setdefault("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1")
os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1")

import numpy as np  # noqa: E402
import pygame  # noqa: E402

_HERE = Path(__file__).resolve().parent
_ROOT = _HERE.parent
sys.path.insert(0, str(_ROOT / "pi05"))
_RPC_PATH = _ROOT / "pi05" / "run_policy_client.py"
_WR_PATH = _ROOT / "dynamic_tracking_limits" / "drag_teaching" / "waypoint_replay.py"


def read_constants(path, names):
    """path を読み込まずに (実行せずに), 一番外の `名前 = 値` の値を読む. 1 回だけ代入されている名前に限る.
    ファイルのどこかで ほかの形 (+= ・型注釈つき・連鎖・for・global の中など) で同じ名前に代入していれば止める
    (--execute では読み込んだ値とも比べる)."""
    found = {}
    tree = ast.parse(Path(path).read_text(encoding="utf-8"))
    simple = set()
    for node in tree.body:
        if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
            name = node.targets[0].id
            if name in names:
                if name in found:
                    raise RuntimeError(f"{path} で {name} が 2 回代入されています")
                found[name] = ast.literal_eval(node.value)
                simple.add(id(node.targets[0]))
    for node in ast.walk(tree):
        if (isinstance(node, ast.Name) and node.id in names and isinstance(node.ctx, (ast.Store, ast.Del))
                and id(node) not in simple):
            raise RuntimeError(f"{path} で {node.id} がほかの形でも代入されています (行 {node.lineno})")
    missing = [n for n in names if n not in found]
    if missing:
        raise RuntimeError(f"{path} に {missing} が見つかりません")
    return found


# 爪 (gripper_cmd)・HOME・J4_SAFE・関節角の読み方 (read_angles) は run_policy_client と同じものを使う.
# daemon の送り口と run_policy_client は --execute のときだけ load_daemon_modules() で読む (cv2 の SDL を避ける)
_RPC_CONST = read_constants(_RPC_PATH, ("HOME", "J4_SAFE", "GRIPPER_SLEEP"))
_WR_CONST = read_constants(_WR_PATH, ("JOINT_LIMITS",))
rpc = None
wr = None
SPEED = POLL_SLEEP = REACH_TIMEOUT_SEC = None     # j0_sweep_scan の 20 / 0.3 / 8.0 (--execute で読む)
close_daemon_socket = daemon_send_angles = daemon_set_gripper = daemon_set_gripper_value = get_daemon_socket = None
daemon_request = None


def load_daemon_modules():
    """--execute のときだけ呼ぶ. run_policy_client (パスと pymycobot のスタブも入る)・j0_sweep_scan・
    visual_servo_pick の daemon の送り口を読む. 2 回目からは何もしない.
    読み込まずに読んだ値 (HOME・J4_SAFE・GRIPPER_SLEEP・JOINT_LIMITS) と違えば止める."""
    global rpc, wr, SPEED, POLL_SLEEP, REACH_TIMEOUT_SEC
    global close_daemon_socket, daemon_send_angles, daemon_set_gripper, daemon_set_gripper_value, get_daemon_socket
    global daemon_request
    if rpc is not None:
        return
    import run_policy_client as _rpc
    import j0_sweep_scan as _j0
    import visual_servo_pick as _vsp
    pairs = (("HOME", _rpc.HOME, HOME), ("J4_SAFE", _rpc.J4_SAFE, J4_SAFE),
             ("GRIPPER_SLEEP", _rpc.GRIPPER_SLEEP, GRIPPER_SLEEP), ("JOINT_LIMITS", _rpc.wr.JOINT_LIMITS, JOINT_LIMITS))
    diff = [n for n, loaded, read in pairs if _norm(loaded) != _norm(read)]
    if diff:
        raise RuntimeError(f"読み込んだ値がファイルから読んだ値と違います: {diff}")
    SPEED, POLL_SLEEP, REACH_TIMEOUT_SEC = _j0.SPEED, _j0.POLL_SLEEP, _j0.REACH_TIMEOUT_SEC
    close_daemon_socket, daemon_send_angles = _vsp.close_daemon_socket, _vsp.daemon_send_angles
    daemon_set_gripper, daemon_set_gripper_value = _vsp.daemon_set_gripper, _vsp.daemon_set_gripper_value
    get_daemon_socket = _vsp.get_daemon_socket
    daemon_request = _vsp._daemon_request
    wr = _rpc.wr
    rpc = _rpc


def _norm(v):
    if isinstance(v, (list, tuple)):
        return [_norm(x) for x in v]
    return float(v)


# ---- ボタンと軸 (/Volumes/KIOXIA/r2r2r-data/diag/gamepad_check_20261102/ で確かめた番号) ----
BTN_CROSS = 0
BTN_CIRCLE = 1
BTN_SQUARE = 2
BTN_TRIANGLE = 3
BTN_SHARE = 4
BTN_HOME = 5
BTN_OPTIONS = 6
BTN_L3 = 7
BTN_R3 = 8
BTN_L1 = 9
BTN_R1 = 10
BTN_UP = 11
BTN_DOWN = 12
BTN_LEFT = 13
BTN_RIGHT = 14
BTN_TOUCHPAD = 15
BUTTON_NAMES = {
    BTN_CROSS: "cross", BTN_CIRCLE: "circle", BTN_SQUARE: "square", BTN_TRIANGLE: "triangle",
    BTN_SHARE: "share", BTN_HOME: "home", BTN_OPTIONS: "options", BTN_L3: "l3", BTN_R3: "r3",
    BTN_L1: "l1", BTN_R1: "r1", BTN_UP: "dpad_up", BTN_DOWN: "dpad_down", BTN_LEFT: "dpad_left",
    BTN_RIGHT: "dpad_right", BTN_TOUCHPAD: "touchpad",
}
AXIS_L2 = 4
AXIS_R2 = 5
TRIGGER_STOP_ABOVE = 0.0      # L2 と R2 の両方がこれを超えたら停止 (離して -1.0, 押し込んで +1.0)

# ---- 動かし方 ----
STEP_FINE_DEG = 1.0            # 2026-11-03: 0.5° はサーボが反応しなかった (読んだ角度が変わらない). 1° にした
STEP_COARSE_DEG = 2.0          # R1 を押しながら
J0_LEFT_SIGN = -1.0            # 十字キー左で j0 を動かす向き (右はその逆). 依頼に向きの指定がない
UP_SIGN = -1.0                 # 十字キー上 = −, 下 = + (PICK の上空 → 床で j1・j2・j4 が増える向き)
SELECT_ORDER = (1, 2, 3, 4, 5)  # △ で回す順. 始めは j1
HOME_HOLD_SEC = 1.0
HOME = [float(v) for v in _RPC_CONST["HOME"]]   # [0, 0, 0, 0, 0, -30]

# ---- 守り ----
J1_MAX_DEG = 90.87             # arm_kinematics.md の PICK_J1_FIXED (エイイチと決めた. J1_SAFE_MAX 88 は使わない)
J4_SAFE = tuple(float(v) for v in _RPC_CONST["J4_SAFE"])   # (-90, 60)
JOINT_LIMITS = [tuple(float(v) for v in lim) for lim in _WR_CONST["JOINT_LIMITS"]]   # 物理可動域 (wr.JOINT_LIMITS)
GRIPPER_SLEEP = float(_RPC_CONST["GRIPPER_SLEEP"])   # 爪のあとの待ち (rpc.GRIPPER_SLEEP. ドライランと画面の表示で使う)
LIMIT_EPS_DEG = 1e-6
REACH_TOL_DEG = 3.0            # 届いたかの判定 (推論の到着確認と同じ 3°)
STEP_REACH_TOL_DEG = 0.5       # 1 刻み (1°・2°) で, 動かした関節が届いたかの判定 (2026-11-05. 3° では動かなくても「届いた」になり,
                               # 1° と 2° の違いが出なかった. 実機の記録で 1 刻みの 30 回中 23 回は 1 ° も動いていなかった)

# ---- 長押し (2026-11-03) ----
HOLD_START_SEC = 0.4           # 押してからこれだけたっても押していれば, 長押しとして送り続ける
STEP_PENDING_MAX_SEC = 1.0     # 離したときの 1 刻みを, 前の仕事が終わるまで待つ上限 (古い押しを後から動かさない)
HOLD_INTERVAL_SEC = 0.1        # 長押しで目標を送り直す間隔
HOLD_AHEAD_DEG = 15.0          # 長押しの目標 = 読んだ角度からこれだけ先 (2026-11-05 に 3 → 15. 離すと stop で止める)
HOLD_REFILL_DEG = 5.0          # 目標まで残りがこれ以下になったら次の目標を送る (送るたびに加速からやり直すので, 送る回数を減らす)
HALT_SETTLE_SEC = 0.1          # stop を送ってから角度を読むまで (11/5 に 0.3 → 0.1. 止める間に押した操作が受付なしになっていた)
STEP_POLL_SEC = 0.05           # 1 刻みで角度を読む間隔 (11/5. 0.3 秒おきでは, 届いたと分かるまで毎回 0.3 秒以上かかった)
STEP_SETTLE_SEC = 0.25         # 1 刻みで動き出したあと, これだけ角度が変わらなければ「止まった」として戻る
                               # (重さで 1〜2° 手前で止まる関節で, 届くのを 8 秒待って受付なしが続いた)
STEP_IGNORED_CHECK_SEC = 0.3   # 1 刻みで動き出さなければ送り直すまで (6 関節の送り方の 0.5 秒より短く)
STEP_MAX_RESENDS = 2           # 1 刻みの送り直しの上限 (動かないときに 3 秒近く待たないように)
HOLD_STALL_SEC = 1.5           # このあいだ動かした関節が HOLD_STALL_DEG 進んでいなければ, 長押しを止める (11/5 に 1.0 → 1.5. 送り直す間を持たせる)
HOLD_RESEND_AFTER_SEC = 0.3    # 長押しで目標の手前で止まったら (この時間たっても前の読みから動いていない), 同じ目標を送り直す
HOLD_MAX_RESENDS = 3           # 1 つの目標を送り直す回数の上限
START_FALLBACK_DEG = 10.0      # 動かさない関節は, 指令と読んだ角度がこれより離れていなければ 指令のまま (重さで 3〜6° 下がるのを拾わない)
HOLD_STALL_DEG = 0.3
DRY_HOLD_MAX_DEG = 1.5         # ドライランの長押しで 1 回読むごとに目標へ寄せる量 (0.1 秒おきに読むので 15°/秒 のつもり)

# ---- 無視された指令の送り直し (2026-11-03) ----
IGNORED_CHECK_SEC = 0.5        # 送ってからこれだけたっても動いていなければ, 無視されたとみなす
IGNORED_MOVE_DEG = 0.3         # どの関節もこれより動いていなければ「動いていない」
IGNORED_MAX_RESENDS = 3        # 送り直す回数の上限

DRY_MOVE_SEC = 0.3             # ドライランで「動いている」状態を見せる時間
SEND_RPC_TIMEOUT_SEC = 8.0     # send_angles 1 回の通信 (接続・ping・送信・返事) の上限. 通信が詰まった
                               # 送信が 停止のずっとあとに届かないように (daemon の返事は送信後の読み直し込みで最長 3.7 秒. LOG352)
WORKER_JOIN_SEC = 12.0         # 終わるとき, 送っている途中の仕事が戻るのを待つ上限 (読み取り 1 回は最長 3.7 秒)

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
LOG_ROOT = DATA_ROOT / "runs" / "experiments" / "gamepad_teleop"
SSD_MOUNT = "/Volumes/KIOXIA"

STATE_IDLE = "IDLE"
STATE_MOVING = "MOVING"
STATE_STOPPED = "STOPPED"


def ensure_ssd_mounted():
    if not os.path.ismount(SSD_MOUNT):
        sys.exit("[ABORT] KIOXIA がマウントされていません。記録をローカルには逃がさず、ここで止めます。")


def _r(a):
    return None if a is None else [round(float(v), 2) for v in a]


def _fmt(a):
    return "None" if a is None else " ".join(f"{float(v):+.2f}" for v in a)


# =============================================================================
# 守り (純関数. テストで直接呼ぶ)
# =============================================================================

def joint_window():
    """各関節に送ってよい範囲 (lo, hi, 各端の出どころ) を作る.
    出どころ: PHYS (物理可動域) / J4_SAFE / J1_MAX. 狭い方が勝つ."""
    lo, hi, lo_src, hi_src = [], [], [], []
    for k in range(6):
        cands_lo = [(JOINT_LIMITS[k][0], "PHYS")]
        cands_hi = [(JOINT_LIMITS[k][1], "PHYS")]
        if k == 4:
            cands_lo.append((J4_SAFE[0], "J4_SAFE"))
            cands_hi.append((J4_SAFE[1], "J4_SAFE"))
        if k == 1:
            cands_hi.append((J1_MAX_DEG, "J1_MAX"))
        v_lo, s_lo = max(cands_lo, key=lambda c: c[0])
        v_hi, s_hi = min(cands_hi, key=lambda c: c[0])
        lo.append(v_lo); hi.append(v_hi); lo_src.append(s_lo); hi_src.append(s_hi)
    return np.array(lo), np.array(hi), lo_src, hi_src


LIMIT_JA = {"PHYS": "物理可動域", "J4_SAFE": "J4_SAFE",
            "J1_MAX": "j1 の上限 90.87 (PICK_J1_FIXED)"}


def start_pose(cmd, measured):
    """次の出発点: 関節ごとに, 指令と読んだ角度が START_FALLBACK_DEG (10°) 以内なら指令, 離れていれば読んだ角度.
    (11/5: 3° にしていたら, 腕を伸ばした姿勢で j1 が重さで 3〜6° 下がり, 読んだ角度を拾ってずれが積み上がった)
    (重さで 1〜2° 沈んだ分を読んだ角度でなぞると, 送るたびにずれが積み上がる. 2026-11-05.
     手で動かされた・止められたなど 3° より離れていれば, 古い指令から大きく動かさない)"""
    cmd = np.asarray(cmd, dtype=float)
    q = np.asarray(measured, dtype=float)
    return np.where(np.abs(cmd - q) <= START_FALLBACK_DEG, cmd, q)


def plan_step(start, joint, delta):
    """start (今回の出発点) の joint を delta 動かした目標を作り, 範囲を通す.

    戻り値 dict:
      target   送る目標 (6) or None (送らない)
      clip     切られた中身 [{joint, want, cmd, limit, bound}] (切られて端まで送るとき)
      refused  送らない理由 [{joint, value, limit, lo, hi, ...}] (空なら送ってよい)
    動かした関節は範囲の端で切る (端までは送る). 端にいて押した向きに 1 ° も進めないときは送らない.
    ほかの関節は変えない (補わない). 6 関節のどれかが範囲の外なら送らない.
    """
    lo, hi, lo_src, hi_src = joint_window()
    want = np.asarray(start, dtype=float)[:6].copy()
    out = {"target": None, "clip": [], "refused": []}
    if not np.all(np.isfinite(want)):
        out["refused"].append({"joint": None, "why": "nonfinite_start", "value": _r(want)})
        return out
    # 出発点の 6 関節がどれか範囲の外なら送らない (切ってから送ると, 範囲の外から 1 回で
    # 刻みより大きく動く. 例: j1=100 から −0.5 を押すと 90.87 へ −9.13° になる. codex 必須 2)
    for k in range(6):
        if want[k] < lo[k] - LIMIT_EPS_DEG or want[k] > hi[k] + LIMIT_EPS_DEG:
            src = hi_src[k] if want[k] > hi[k] else lo_src[k]
            out["refused"].append({"joint": k, "why": "start_out_of_window", "value": round(want[k], 2),
                                   "lo": round(lo[k], 2), "hi": round(hi[k], 2), "limit": src})
    if out["refused"]:
        return out
    v = want[joint] + delta
    if lo[joint] > hi[joint]:
        out["refused"].append({"joint": joint, "why": "empty_window", "value": round(v, 2),
                               "lo": round(lo[joint], 2), "hi": round(hi[joint], 2),
                               "limit": f"{lo_src[joint]}/{hi_src[joint]}"})
        return out
    c = min(max(v, lo[joint]), hi[joint])
    if abs(c - v) > LIMIT_EPS_DEG:
        src = hi_src[joint] if v > hi[joint] else lo_src[joint]
        bound = hi[joint] if v > hi[joint] else lo[joint]
        out["clip"].append({"joint": joint, "want": round(v, 2), "cmd": round(c, 2),
                            "limit": src, "bound": round(float(bound), 2)})
    if (c - want[joint]) * np.sign(delta) <= LIMIT_EPS_DEG:
        # 端にいる (または出発点が範囲の外で, 押した向きに進めない)
        src = hi_src[joint] if delta > 0 else lo_src[joint]
        out["refused"].append({"joint": joint, "why": "at_limit", "value": round(v, 2),
                               "lo": round(lo[joint], 2), "hi": round(hi[joint], 2), "limit": src})
        return out
    want[joint] = c
    for k in range(6):
        if want[k] < lo[k] - LIMIT_EPS_DEG or want[k] > hi[k] + LIMIT_EPS_DEG:
            src = hi_src[k] if want[k] > hi[k] else lo_src[k]
            out["refused"].append({"joint": k, "why": "out_of_window", "value": round(want[k], 2),
                                   "lo": round(lo[k], 2), "hi": round(hi[k], 2), "limit": src})
    if not out["refused"]:
        out["target"] = want
    return out


def limit_text_ascii(plan):
    """画面用 (英数字だけ)."""
    parts = []
    for c in plan.get("clip", []):
        parts.append(f"j{c['joint']} clipped {c['want']:+.2f}->{c['cmd']:+.2f} ({c['limit']} {c['bound']:+.2f})")
    for r in plan.get("refused", []):
        if r.get("joint") is None:
            parts.append(f"refused: {r['why']}")
        else:
            parts.append(f"j{r['joint']} refused {r['why']} {r['value']:+.2f} "
                         f"[{r['lo']:+.2f}, {r['hi']:+.2f}] ({r['limit']})")
    return " / ".join(parts)


def limit_text_ja(plan):
    """ターミナル用 (日本語)."""
    parts = []
    for c in plan.get("clip", []):
        parts.append(f"j{c['joint']} を {c['want']:+.2f}° → {c['cmd']:+.2f}° に切った"
                     f"（{LIMIT_JA.get(c['limit'], c['limit'])} {c['bound']:+.2f}°）")
    for r in plan.get("refused", []):
        if r.get("joint") is None:
            parts.append(f"送らない: {r['why']}")
            continue
        why = {"at_limit": "範囲の端にいて、押した向きに進めない",
               "out_of_window": "範囲の外",
               "start_out_of_window": "出発点が範囲の外（HOME の長押しで戻せます）",
               "empty_window": "範囲が空"}.get(r["why"], r["why"])
        parts.append(f"j{r['joint']} {r['value']:+.2f}° は送らない：{why}"
                     f"（範囲 [{r['lo']:+.2f}, {r['hi']:+.2f}]・{LIMIT_JA.get(r['limit'], r['limit'])}）")
    return " / ".join(parts)


# =============================================================================
# アームへの送り口
# =============================================================================

class SendGate:
    """1 つの仕事の「送ると決める」と「停止・終了の取り消し」を順番に並べる鍵 (codex R1 必須 1 / R2).

    送る側は 鍵の中で取り消しを確かめて「送ると決めた」(committed) を立て, 鍵の外で送る.
    停止・終了は 鍵の中で取り消しを立て, そのときに送ると決めていたかを受け取る.
    鍵を持つのは確かめる一瞬だけなので, 停止は待たされない (画面も止まらない).
    なので「停止のあとに送ると決めた指令」はない. 停止より前に決めた送信は, 通信が遅れても
    daemon_send_angles の timeout_override (SEND_RPC_TIMEOUT_SEC) の中で終わる.
    取り消しは仕事ごとに作り, OPTIONS で再開しても戻さない (止める前の仕事が生き返らない).
    """

    def __init__(self):
        self.lock = threading.Lock()
        self.cancel = threading.Event()
        self.committed = False

    def run(self, fn):
        """取り消されていなければ「送ると決めた」を立ててから fn() を呼ぶ. 戻り値: 呼んだか."""
        with self.lock:
            if self.cancel.is_set():
                return False
            self.committed = True
        fn()
        return True

    def cancel_now(self):
        """取り消しを立てる. 戻り値: それより前に送ると決めていたか (送信中・送信済み)."""
        with self.lock:
            self.cancel.set()
            return self.committed


class DaemonArm:
    """--execute: daemon に送る. 送り方・読み方は visual_servo_pick / run_policy_client と同じ."""

    execute = True

    def read(self):
        return rpc.read_angles()          # 5 回まで読み直す. 読めなければ None

    @staticmethod
    def _send(target, joint):
        """joint があれば その関節だけ send_angle で送る (ほかの関節には指令を送らない. 2026-11-05 の jog_probe small で
        12 回中 12 回動き, 動いた量もそろった. 6 関節まとめて送ると, 重さで下がった関節の指令とぶつかって
        ゲームパッドでは動かないことが多かった). joint が None (HOME) なら send_angles で 6 関節."""
        if joint is None:
            return daemon_send_angles([float(v) for v in target], speed=SPEED, wait=rpc.SEND_WAIT,
                                      timeout_override=SEND_RPC_TIMEOUT_SEC)
        resp = daemon_request({"cmd": "send_angle", "joint_id": int(joint) + 1, "angle": float(target[joint]),
                               "speed": SPEED, "wait": rpc.SEND_WAIT}, timeout_override=SEND_RPC_TIMEOUT_SEC)
        if not resp.get("ok"):
            raise RuntimeError(f"send_angle: {resp.get('error')}")
        return resp

    def send_and_check(self, target, gate, quitting, check=True, joint=None):
        """target を send_angles (speed 20) で送り, 3° 以内に届くかを読んで確かめる.
        gate が取り消されていれば送らない. 無視されたら送り直す (IGNORED_*). 確かめる途中で読めなければ error.
        check=False (長押し): 送るだけで, 届いたかは待たない (0.1 秒後に次の目標を送るため)."""
        target = np.asarray(target, dtype=float)
        if not check:
            t_send = time.time()
            try:
                sent = gate.run(lambda: self._send(target, joint))
            except Exception as e:
                return {"sent": "unknown", "error": f"send_failed: {type(e).__name__}: {e}", "t_send": t_send}
            if not sent:
                return {"sent": False, "why": "halted_before_send"}
            return {"sent": True, "reached": None, "no_check": True, "t_send": t_send}
        pre = self.read()
        if pre is None:
            return {"sent": False, "error": "read_failed_before_send"}
        pre_max = float(np.max(np.abs(target - pre)))
        timeout = max(REACH_TIMEOUT_SEC, wr._compute_arrival_timeout(pre_max))
        t_send = time.time()
        try:
            sent = gate.run(lambda: self._send(target, joint))
        except Exception as e:
            # 届いたかどうか分からない. 呼び出し側で停止にする
            return {"sent": "unknown", "error": f"send_failed: {type(e).__name__}: {e}",
                    "measured_before_send": _r(pre), "t_send": t_send}
        if not sent:
            return {"sent": False, "why": "halted_before_send", "measured_before_send": _r(pre)}
        t0 = time.monotonic()
        t_last_send = t0
        last, err = None, None
        step = joint is not None
        check_sec = STEP_IGNORED_CHECK_SEC if step else IGNORED_CHECK_SEC
        max_resends = STEP_MAX_RESENDS if step else IGNORED_MAX_RESENDS
        poll = STEP_POLL_SEC if step else POLL_SLEEP
        q_seen, t_change = None, t0          # 1 刻み: 動かした関節の角度が最後に変わった時刻
        resends = []                      # 送り直した時刻 (送ってからの秒)
        base = {"sent": True, "timeout_sec": round(timeout, 2), "t_send": t_send, "measured_before_send": _r(pre),
                "resends": resends}
        while time.monotonic() - t0 < timeout:
            if quitting.is_set():
                return dict(base, reached=None, interrupted=True, reached_angles=_r(last),
                            reach_err=None if err is None else round(err, 3))
            if gate.cancel.is_set():
                # 停止 (L2+R2) が入った: 届くのを待たずに戻り, すぐに stop を送らせる (2026-11-05 レビュー 必須 1)
                return dict(base, reached=None, halted_during_check=True, reached_angles=_r(last),
                            reach_err=None if err is None else round(err, 3),
                            elapsed_sec=round(time.monotonic() - t0, 2))
            a = self.read()
            if a is None:
                # 動いているはずの間に読めない: 届いたか分からないまま次を受け付けない (codex 必須 6)
                return dict(base, reached=None, reached_angles=_r(last),
                            reach_err=None if err is None else round(err, 3),
                            error="read_failed_during_check", elapsed_sec=round(time.monotonic() - t0, 2))
            last = a
            err = float(np.max(np.abs(a - target)))          # 丸める前の値で判定する (codex 推奨 3)
            err_j = None if joint is None else abs(float(a[joint] - target[joint]))
            # 1 刻みは 動かした関節だけで判定する (ほかの関節は重さで 3〜6° 下がっていても「届いた」にする. 11/5)
            if (err_j <= STEP_REACH_TOL_DEG) if err_j is not None else (err <= REACH_TOL_DEG):
                return dict(base, reached=True, reached_angles=_r(a), reach_err=round(err, 3),
                            elapsed_sec=round(time.monotonic() - t0, 2))
            moved = float(np.max(np.abs(a - pre))) if joint is None else abs(float(a[joint] - pre[joint]))
            if step:
                if q_seen is None or abs(float(a[joint]) - q_seen) > 0.05:
                    q_seen, t_change = float(a[joint]), time.monotonic()
                elif moved >= IGNORED_MOVE_DEG and time.monotonic() - t_change >= STEP_SETTLE_SEC:
                    # 動き出して止まった (重さで手前で止まった): 待たずに戻る. 指令はこの目標のまま (次の押しはこの先から)
                    return dict(base, reached=False, settled=True, reached_angles=_r(a), reach_err=round(err, 3),
                                elapsed_sec=round(time.monotonic() - t0, 2))
            if (len(resends) >= max_resends and moved < IGNORED_MOVE_DEG
                    and time.monotonic() - t_last_send >= check_sec):
                # 送り直しても動かない: 待ちの上限 (8 秒) まで待たずに戻る
                return dict(base, reached=False, not_moved=True, reached_angles=_r(a), reach_err=round(err, 3),
                            elapsed_sec=round(time.monotonic() - t0, 2))
            if (len(resends) < max_resends and moved < IGNORED_MOVE_DEG
                    and time.monotonic() - t_last_send >= check_sec):
                # 無視された: 同じ目標を送り直す (停止・離したあとなら送らない)
                try:
                    again = gate.run(lambda: self._send(target, joint))
                except Exception as e:
                    return dict(base, reached=None, reached_angles=_r(a), reach_err=round(err, 3),
                                error=f"resend_failed: {type(e).__name__}: {e}",
                                elapsed_sec=round(time.monotonic() - t0, 2))
                if not again:
                    return dict(base, reached=None, halted_during_check=True, reached_angles=_r(a),
                                reach_err=round(err, 3), elapsed_sec=round(time.monotonic() - t0, 2))
                t_last_send = time.monotonic()
                resends.append(round(t_last_send - t0, 2))
                continue
            time.sleep(poll)
        return dict(base, reached=False, reached_angles=_r(last),
                    reach_err=None if err is None else round(err, 3),
                    elapsed_sec=round(time.monotonic() - t0, 2))

    def halt(self):
        """動いている途中で止める (2026-11-05). daemon の stop (jog_stop と stop). stop のない古い daemon なら
        今の角度へ send_angles する. そのあと HALT_SETTLE_SEC 待って角度を読む. 停止の鍵 (SendGate) は通さない
        (止める命令は停止のあとにも送る). 送れなくても停止にはしない (止める → 失敗 → 止める の繰り返しを避ける)."""
        t_send = time.time()
        out = {"t_halt": t_send}
        try:
            resp = daemon_request({"cmd": "stop"}, timeout_override=SEND_RPC_TIMEOUT_SEC)
        except Exception as e:
            resp = None
            out["halt_error"] = f"{type(e).__name__}: {e}"
        if resp is not None and resp.get("ok"):
            out.update(halt="stop", halt_result=resp.get("result"))
        else:
            if resp is not None:
                out["halt_daemon_resp"] = resp
            q = self.read()
            if q is not None:
                try:
                    daemon_send_angles([float(v) for v in q], speed=SPEED, wait=rpc.SEND_WAIT,
                                       timeout_override=SEND_RPC_TIMEOUT_SEC)
                    out["halt"] = "send_current"
                except Exception as e:
                    out["halt_error"] = f"{type(e).__name__}: {e}"
            else:
                out["halt_error"] = out.get("halt_error") or "read_failed"
        time.sleep(HALT_SETTLE_SEC)
        out["measured_after_halt"] = _r(self.read_once())    # 読めなくても読み直さない (11/5. 止める処理が 11 秒かかった)
        return out

    def read_once(self):
        """daemon に 1 回だけ角度を聞く (Mac 側では読み直さない). 読めなければ None."""
        try:
            resp = daemon_request({"cmd": "get_angles"}, timeout_override=SEND_RPC_TIMEOUT_SEC)
        except Exception:
            return None
        a = resp.get("angles") if resp.get("ok") else None
        return np.array([float(v) for v in a]) if a is not None and len(a) == 6 else None

    def gripper(self, state, gate):
        """run_policy_client.gripper_cmd と同じ中身 (閉 = daemon_set_gripper_value(GRIP_VALUE) /
        開 = daemon_set_gripper(0, wait=GRIP_OPEN_DAEMON_WAIT). そのあと GRIPPER_SLEEP 待つ).
        指令だけを gate の中で送り, 待ちは gate の外で行う (停止の受け付けを待ちの分だけ遅らせない).
        戻り値: 送ったか."""
        if state == 1:
            sent = gate.run(lambda: daemon_set_gripper_value(rpc.GRIP_VALUE))
        else:
            sent = gate.run(lambda: daemon_set_gripper(0, wait=rpc.GRIP_OPEN_DAEMON_WAIT))
        if sent:
            time.sleep(rpc.GRIPPER_SLEEP)
        return sent


class DryArm:
    """ドライラン: daemon にもアームにも触らない. HOME から始まる仮の姿勢を動かす."""

    execute = False

    def __init__(self, start=HOME):
        self.q = np.asarray(start, dtype=float).copy()
        self.hold_target = None           # 長押しで送った目標. 読むたびに DRY_HOLD_MAX_DEG ずつ寄せる

    def read(self):
        if self.hold_target is not None:
            self.q = self.q + np.clip(self.hold_target - self.q, -DRY_HOLD_MAX_DEG, DRY_HOLD_MAX_DEG)
        return self.q.copy()

    def halt(self):
        self.hold_target = None
        return {"halt": "dry", "measured_after_halt": _r(self.q), "dry": True}

    def send_and_check(self, target, gate, quitting, check=True, joint=None):
        t_send = time.time()
        if not gate.run(lambda: None):
            return {"sent": False, "why": "halted_before_send", "measured_before_send": _r(self.q)}
        if not check:                     # 長押し: 読むたびに少しずつ目標へ寄せる (待たない)
            self.hold_target = np.asarray(target, dtype=float).copy()
            return {"sent": True, "reached": None, "no_check": True, "t_send": t_send, "dry": True}
        self.hold_target = None
        time.sleep(DRY_MOVE_SEC)
        self.q = np.asarray(target, dtype=float).copy()
        return {"sent": True, "reached": True, "reached_angles": _r(self.q), "reach_err": 0.0,
                "t_send": t_send, "dry": True}

    def gripper(self, state, gate):
        if not gate.run(lambda: None):
            return False
        time.sleep(GRIPPER_SLEEP)         # 実機と同じ間だけ受け付けない
        return True


# =============================================================================
# 記録
# =============================================================================

class JsonlLog:
    def __init__(self, path):
        self.path = Path(path)
        self.f = open(self.path, "x", encoding="utf-8")

    def write(self, rec):
        self.f.write(json.dumps(rec, ensure_ascii=False) + "\n")
        self.f.flush()                    # 途中で止まっても残るように 1 行ごとに

    def close(self):
        if not self.f.closed:
            self.f.close()


class NullLog:
    """ドライラン: 何も書かない (依頼 69)."""

    def write(self, rec):
        pass

    def close(self):
        pass


def write_json_atomic(path, obj):
    tmp = Path(str(path) + ".tmp")
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(obj, f, ensure_ascii=False, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(tmp, path)


# =============================================================================
# 操作の状態 (pygame に依存しない. テストで直接呼ぶ)
# =============================================================================

class Teleop:
    """押したボタンを受けて, 送る仕事を別のスレッドで 1 つずつ行う.

    送っている間 (busy) の動く操作は受け付けない (ためない). 停止はいつでも受け付ける.
    状態を書き換えるのは呼び出し側のスレッド (pygame のループ) だけ. 送る仕事の結果は
    results に入り, poll() で受け取って状態とログに反映する.
    送る直前の確かめは 仕事ごとの SendGate で行い, 停止・終了はその鍵を取ってから取り消す.
    """

    def __init__(self, arm, start_pose, log, out=print, spawn=None):
        self.arm = arm
        self.log = log
        self.out = out
        self.spawn = spawn or self._spawn_thread
        self.cmd = np.asarray(start_pose, dtype=float).copy()        # 直前の目標 (始めは起動時の姿勢)
        self.cmd_reached = True
        self.measured = np.asarray(start_pose, dtype=float).copy()   # 最後に読めた角度
        self.sel = 0                       # SELECT_ORDER の何番目 (始めは j1)
        self.r1 = False
        self.stopped = False
        self.stop_reason = None
        self.busy = False
        self.pending = None                # 送っている操作のログ行
        self.thread = None
        self.grip = "unknown"
        self.last_limit = ""               # 直前に範囲で切られた理由 (画面用. 英数字)
        self.last_msg = ""
        self.gate = None                   # 今の仕事の SendGate (停止・終了で取り消す)
        self.log_failed = False            # log.jsonl に書けなくなったら 再開させない (終わらせる)
        self.quitting = threading.Event()
        self.results = queue.Queue()
        self.home_down_at = None
        self.home_handled = False
        self.triggers = [-1.0, -1.0]
        self.both_triggers = False
        self.read_error = False
        self.seq = 0
        self.n_sent = 0
        self.hold = None                   # 十字キーの長押し {id, button, joint, sign, down_at, n, last_req_at}
        self.hold_seq = 0
        self.hold_last_sent_id = None      # 長押しの目標を送った長押しの id (その長押しが終わったら止める)
        self.hold_sent = None              # 最後に送った長押しの目標 {q (送る前に読んだ角度), t, resent}
        self.halt_pending = None           # 止める命令を送る予定 {reason, hold_id, measured_at_request}
        self.step_pending = None           # 離したときの 1 刻みで, 前の仕事が終わるのを待っているもの

    # ---- 見せる値 ----
    @property
    def joint(self):
        return SELECT_ORDER[self.sel]

    @property
    def step(self):
        return STEP_COARSE_DEG if self.r1 else STEP_FINE_DEG

    @property
    def state(self):
        if self.stopped:
            return STATE_STOPPED
        return STATE_MOVING if self.busy else STATE_IDLE

    # ---- 記録 ----
    def _entry(self, button, kind, **kw):
        self.seq += 1
        e = {"seq": self.seq, "t": datetime.now().isoformat(timespec="milliseconds"),
             "t_unix": round(time.time(), 3), "button": button,
             "button_name": BUTTON_NAMES.get(button) if isinstance(button, int) else button,
             "kind": kind, "selected_joint": self.joint, "step_deg": self.step,
             "execute": bool(self.arm.execute)}
        e.update(kw)
        return e

    def _finish(self, e):
        e.update({"grip_state": self.grip, "stopped": self.stopped, "stop_reason": self.stop_reason})
        try:
            self.log.write(e)
        except Exception as ex:          # SSD が外れたなど. ローカルには逃がさない
            first = not self.log_failed
            self.log_failed = True
            if first:
                self.out(f"[記録エラー] log.jsonl に書けません: {ex}。停止にします"
                         "（再開はできません。終わらせて KIOXIA を確かめてください）")
            if not self.stopped:
                self.stop_reason = "log_write_failed"
            self.stopped = True
            self._cancel_gate()
            self._drop_home_hold()

    # ---- 停止 ----
    def _cancel_gate(self):
        """今の仕事をまだ送ると決めていなければ, もう送らせない (待たない).
        戻り値: 停止より前に送ると決めていたか (None = 仕事なし)."""
        if self.gate is None:
            return None
        return self.gate.cancel_now()

    def _drop_home_hold(self):
        """HOME の長押しを無効にする (押し直しを求める. codex 必須 4)."""
        if self.home_down_at is not None and not self.home_handled:
            self.home_handled = True
            self.out("[HOME] 長押しの途中で受け付けられなくなったので取り消しました（押し直してください）")
            self._finish(self._entry(BTN_HOME, "home", accepted=False, reason="hold_cancelled"))

    def _end_hold(self, msg=None, reason="hold_end"):
        h = self.hold
        if h is not None:
            if h.get("q0") is not None:
                self.out(f"[長押し] j{h['joint']} を {h['q0']:+.2f}° → {h['q_last']:+.2f}° まで"
                         f"（目標を {h['n_sent']} 回送信。止めます）")
            if msg:
                self.out(msg)
            if self.hold_last_sent_id == h["id"]:
                # 長押しの目標 (最大 15° 先) へ向かって動いているので止める (離した・停止・端・進まない・ほかのボタン)
                self.request_halt(reason, hold_id=h["id"], joint=h["joint"])
            self.hold = None

    def request_halt(self, reason, hold_id=None, joint=None):
        """止める命令を送る予定にする. 送っている仕事があれば, それが終わってすぐに送る (daemon の通信は 1 本なので).
        joint: 長押しで動かしていた関節 (止めたあと, ほかの関節を指令の角度へ戻すため)."""
        if self.halt_pending is None:
            self.halt_pending = {"reason": reason, "hold_id": hold_id, "joint": joint,
                                 "measured_at_request": _r(self.measured)}
        self.hold_last_sent_id = None
        self.hold_sent = None
        self._dispatch_halt()

    def _dispatch_halt(self):
        if self.halt_pending is None or self.busy:
            return
        hp = self.halt_pending
        self.halt_pending = None
        e = self._entry(None, "halt", accepted=True, halt_reason=hp["reason"], hold_id=hp["hold_id"],
                        halt_joint=hp.get("joint"), measured_at_request=hp["measured_at_request"])
        self.busy = True
        self.pending = e
        self.gate = None                   # 止める命令は停止の鍵で取り消さない
        snap = {"cmd": self.cmd.copy(), "cmd_reached": self.cmd_reached, "hold_last_sent_id": None,
                "halt_joint": hp.get("joint")}
        self.spawn(lambda: self._run_job(("halt",), snap, SendGate()))

    def _hold_progress(self, e, job):
        """長押しの 1 回の送信のあと: 進み具合を覚え, HOLD_STALL_SEC 進んでいなければ止める."""
        h = self.hold
        if h is None or h["id"] != e.get("hold_id") or not e.get("measured_before"):
            return
        qj = float(e["measured_before"][job[1]])
        t = h["last_req_at"]
        if h.get("q0") is None:
            h.update(q0=qj, n_sent=0, prog_q=qj, prog_t=t)
        h["q_last"] = qj
        if e.get("sent") is True:
            h["n_sent"] += 1
        if abs(qj - h["prog_q"]) >= HOLD_STALL_DEG:
            h["prog_q"], h["prog_t"] = qj, t
        elif t - h["prog_t"] >= HOLD_STALL_SEC:
            self._end_hold(f"[長押し] j{job[1]} が {HOLD_STALL_SEC:.0f} 秒進まないので止めました"
                           "（どこかに当たっていれば、逆向きを1回押すか HOME で戻してください）", reason="hold_stall")

    def enter_stop(self, reason, button=None, kind="stop"):
        was = self.stopped
        self.stopped = True
        self._end_hold(reason="stop")
        committed = self._cancel_gate()
        if not was:
            self.stop_reason = reason
        self.out(f"[停止] {reason}。新しい指令は送りません（OPTIONS で再開）"
                 + ("。止める前に送った動きは stop で止めます" if committed else "")
                 + ("。まだ送っていなかった操作は送りません" if committed is False else ""))
        self._finish(self._entry(button, kind, accepted=True, reason=reason, already_stopped=was,
                                 busy=self.busy, send_committed_before_stop=committed,
                                 triggers=[round(v, 3) for v in self.triggers]))
        self._drop_home_hold()
        if not was:
            # 動いている途中の動き (長押し・HOME・1 刻み) を止める. 止まっている腕に送っても何も起きない
            self.request_halt(reason)

    def set_triggers(self, l2, r2):
        self.triggers = [float(l2), float(r2)]
        both = l2 > TRIGGER_STOP_ABOVE and r2 > TRIGGER_STOP_ABOVE
        if both and not self.both_triggers:
            self.both_triggers = True
            self.enter_stop("L2+R2", button="L2+R2")
        self.both_triggers = both

    def controller_lost(self, reason):
        self.enter_stop(reason, button=None, kind="controller_lost")
        self.r1 = False                      # 押しっぱなしの状態を持ち越さない
        self.home_down_at = None
        self.home_handled = False
        self.both_triggers = False

    def controller_read_failed(self, msg):
        if not self.read_error:
            self.read_error = True
            self.enter_stop(f"read_error: {msg}", button=None, kind="read_error")

    def controller_read_ok(self):
        self.read_error = False

    def _resume(self, e):
        if not self.stopped:
            e.update(accepted=False, reason="not_stopped")
            self.out("[再開] 止まっていないので何もしません")
        elif self.both_triggers:
            e.update(accepted=False, reason="triggers_still_pressed")
            self.out("[再開] L2 と R2 を離してから OPTIONS を押してください")
        elif self.read_error:
            e.update(accepted=False, reason="controller_read_error")
            self.out("[再開] コントローラーを読めていないので再開しません")
        elif self.log_failed:
            e.update(accepted=False, reason="log_write_failed")
            self.out("[再開] 記録が書けないので再開しません（終わらせて KIOXIA を確かめてください）")
        else:
            # 止める前に作った仕事の取り消しは戻さない (仕事ごとの SendGate. 再開で生き返らない)
            self.stopped = False
            self.stop_reason = None
            e.update(accepted=True)
            self.out("[再開] 操作を受け付けます")
        self._finish(e)

    # ---- ボタン ----
    def press(self, button, now):
        if button == BTN_R1:
            self.r1 = True
            self._finish(self._entry(button, "modifier", accepted=True))
            return
        self._end_hold()                     # 十字キーの長押しは, ほかのボタン (R1 以外) で止まる
        if button == BTN_TRIANGLE:
            self.sel = (self.sel + 1) % len(SELECT_ORDER)
            self.out(f"[関節] 上下で動かす関節を j{self.joint} にしました")
            self._finish(self._entry(button, "select_joint", accepted=True))
            return
        if button == BTN_OPTIONS:
            self._resume(self._entry(button, "resume"))
            return
        if button == BTN_HOME:
            self.home_down_at = now
            self.home_handled = False
            if self.stopped or self.busy:
                # 押し始めに受け付けられないなら, 長押しを続けても送らない (押し直しを求める)
                self.home_handled = True
                why = "stopped" if self.stopped else "busy"
                self.out("[受付なし] HOME: " + ("停止中です" if self.stopped else "前の操作が終わっていません")
                         + "（押し直してください）")
                self._finish(self._entry(button, "home", accepted=False, reason=why))
            return                           # 1 秒続いたとき (tick) か 離したとき (release) に 1 行書く
        if button in (BTN_LEFT, BTN_RIGHT, BTN_UP, BTN_DOWN):
            if button in (BTN_LEFT, BTN_RIGHT):
                sign = J0_LEFT_SIGN if button == BTN_LEFT else -J0_LEFT_SIGN
                j = 0
            else:
                sign = UP_SIGN if button == BTN_UP else -UP_SIGN
                j = self.joint
            if self.stopped:
                self._request(self._entry(button, "move", joint=j, delta=sign * self.step), ("move", j, 0.0, None))
                return                       # 停止中: 受付なしを 1 行書くだけ (_request が断る)
            # 2026-11-05: 押した瞬間には送らない. 離したのが HOLD_START_SEC より前なら 1 刻み (release),
            # 押し続けたら長押し (tick) の, どちらか片方だけを送る. 押した瞬間に 1 刻みを送ると,
            # 向きを変えるたびに「1° 動く → 止まる → 動き出す」になり, 前の長押しを止める処理とも重なって受付なしになった
            self.hold_seq += 1
            self.hold = {"id": self.hold_seq, "button": button, "joint": j, "sign": sign,
                         "down_at": now, "n": 0, "last_req_at": now - HOLD_INTERVAL_SEC}
            return
        if button in (BTN_CROSS, BTN_CIRCLE):
            state = 1 if button == BTN_CROSS else 0
            self._request(self._entry(button, "grip_close" if state else "grip_open", grip_cmd=state),
                          ("grip", state))
            return
        self._finish(self._entry(button, "ignored", accepted=False, reason="unassigned"))

    def release(self, button, now):
        if self.hold is not None and button == self.hold["button"] and self.hold["n"] == 0:
            # 長押しになる前に離した: 1 刻みを送る (動いている間なら, 終わってから送る. 1 秒たったら捨てる)
            h = self.hold
            self.hold = None
            self.step_pending = {"button": button, "joint": h["joint"], "delta": h["sign"] * self.step,
                                 "hold_id": h["id"], "at": now}
            self._dispatch_step(now)
        elif self.hold is not None and button == self.hold["button"]:
            hid = self.hold["id"]
            self._end_hold(reason="release")   # 離したら長押しを止める (目標を送っていれば stop も送る)
            p = self.pending
            if (self.busy and self.gate is not None and p is not None
                    and p.get("hold_id") == hid and (p.get("hold_n") or 0) >= 1):
                # 長押しの 2 回目からの刻みを, まだ送ると決めていなければ送らない (1 回目 = 短い押しは取り消さない)
                p["released_during_job"] = True
                self.gate.cancel_now()
        if button == BTN_R1:
            self.r1 = False
        elif button == BTN_HOME:
            if self.home_down_at is not None and not self.home_handled:
                held = now - self.home_down_at
                self.out(f"[HOME] {held:.2f} 秒で離したので送りません（{HOME_HOLD_SEC:.0f} 秒の長押しで送ります）")
                self._finish(self._entry(button, "home_short", accepted=False, reason="held_too_short",
                                         held_sec=round(held, 3)))
            self.home_down_at = None
            self.home_handled = False

    def _dispatch_step(self, now=None):
        sp = self.step_pending
        if sp is None or self.busy or self.halt_pending is not None:
            return
        self.step_pending = None
        if now is not None and now - sp["at"] > STEP_PENDING_MAX_SEC:
            self._finish(self._entry(sp["button"], "move", joint=sp["joint"], delta=sp["delta"],
                                     accepted=False, reason="step_expired"))
            return
        e = self._entry(sp["button"], "move", joint=sp["joint"], delta=sp["delta"], hold_id=sp["hold_id"], hold_n=0)
        self._request(e, ("move", sp["joint"], sp["delta"], None))

    def tick(self, now):
        self._dispatch_halt()
        self._dispatch_step(now)
        if self.home_down_at is not None and not self.home_handled and (self.busy or self.stopped):
            self._drop_home_hold()           # 長押しの途中で別の操作が動いた・止まった
        if (self.home_down_at is not None and not self.home_handled
                and now - self.home_down_at >= HOME_HOLD_SEC):
            self.home_handled = True
            self._request(self._entry(BTN_HOME, "home", held_sec=round(now - self.home_down_at, 3)),
                          ("home",))
        h = self.hold
        if (h is not None and not self.stopped and not self.busy
                and now - h["down_at"] >= HOLD_START_SEC - 1e-6
                and now - h["last_req_at"] >= HOLD_INTERVAL_SEC - 1e-6):
            h["n"] += 1
            h["last_req_at"] = now
            d = h["sign"] * HOLD_AHEAD_DEG
            self._request(self._entry(h["button"], "move", joint=h["joint"], delta=d,
                                      hold_id=h["id"], hold_n=h["n"],
                                      held_sec=round(now - h["down_at"], 3)),
                          ("move", h["joint"], d, h["id"]))

    def _hold_alive(self, hold_id):
        h = self.hold
        return h is not None and h["id"] == hold_id

    # ---- 送る仕事 ----
    def _request(self, e, job):
        if self.stopped:
            e.update(accepted=False, reason="stopped")
            self.out("[受付なし] 停止中です（OPTIONS で再開）")
            self._finish(e)
            return
        if self.busy:
            e.update(accepted=False, reason="busy")
            self.out("[受付なし] 前の操作が終わっていません（ためません）")
            self._finish(e)
            return
        e["accepted"] = True
        self.busy = True
        self.pending = e
        gate = SendGate()
        self.gate = gate
        snap = {"cmd": self.cmd.copy(), "cmd_reached": self.cmd_reached,
                "hold_last_sent_id": self.hold_last_sent_id,
                "hold_sent": None if self.hold_sent is None else dict(self.hold_sent)}
        self.spawn(lambda: self._run_job(job, snap, gate))

    def _spawn_thread(self, fn):
        self.thread = threading.Thread(target=fn, daemon=True)
        self.thread.start()

    def _run_job(self, job, snap, gate):
        res = {}
        try:
            if job[0] == "move":
                _, j, d, hold_id = job
                q = self.arm.read()
                res["measured_before"] = _r(q)
                if q is None:
                    res["error"] = "read_failed"
                    return
                if hold_id is not None:
                    # 長押しの 2 回目から: 動かす関節は「いま読んだ角度 + 3° 先」. ほかの関節は直前の指令のまま
                    # (読んだ角度で送り直すと, 重さで沈んだ分を毎回なぞって j1 などがずり下がる)
                    cmd = np.asarray(snap["cmd"], dtype=float)
                    others = [k for k in range(6) if k != j]
                    gap_o = float(np.max(np.abs(cmd[others] - q[others])))
                    start = start_pose(cmd, q)          # ほかの関節は 関節ごとに 指令から (3° 以内なら)
                    start[j] = q[j]
                    res["start_source"] = "hold" if gap_o <= REACH_TOL_DEG else "mixed"
                    res["cmd_gap_deg"] = round(gap_o, 3)
                    res["hold_lead_deg"] = round(abs(float(cmd[j] - q[j])), 3)
                    lead = float(cmd[j] - q[j]) * float(np.sign(d))
                    hs = snap.get("hold_sent")
                    if snap.get("hold_last_sent_id") == hold_id and lead > HOLD_REFILL_DEG:
                        if (hs is not None and hs["resends"] < HOLD_MAX_RESENDS
                                and time.monotonic() - hs["t"] >= HOLD_RESEND_AFTER_SEC
                                and abs(float(q[j]) - float(hs["last_q"][j])) < IGNORED_MOVE_DEG):
                            # 目標の手前で止まった (次の目標が無視され, 前の目標で止まった. 11/5 の実機で 3 回):
                            # 待たずに同じ目標を送り直す
                            res["command"] = _r(cmd)
                            res["_target"] = cmd.copy()
                            res["hold_resend"] = True
                            res.update(self.arm.send_and_check(cmd, gate, self.quitting, check=False, joint=j))
                            return
                        # この長押しの目標へ向かっている途中: 送らない (送るたびに加速からやり直してカクつく)
                        res.update(sent=False, why="lead_ok", hold_remaining_deg=round(lead, 3))
                        return
                    plan = plan_step(start, j, d)
                    if (plan["target"] is not None and plan["clip"]
                            and abs(float(plan["target"][j] - cmd[j])) < 0.01):
                        if snap.get("hold_last_sent_id") == hold_id and lead > REACH_TOL_DEG:
                            # 範囲の端の目標へ向かっている途中: 端まで行かせる (ここで止めると端の手前 5° で止まる)
                            res.update(sent=False, why="lead_ok", hold_remaining_deg=round(lead, 3), at_limit_target=True)
                            return
                        # 範囲の端の目標をもう送ってあり, ほぼ着いた: 同じ目標を送り続けない (長押しを止める)
                        c = plan["clip"][0]
                        plan = {"target": None, "clip": [], "refused": [{
                            "joint": j, "why": "at_limit", "value": c["want"], "lo": c["bound"], "hi": c["bound"],
                            "limit": c["limit"]}]}
                    if (plan["target"] is not None and abs(float(cmd[j] - q[j])) <= HOLD_AHEAD_DEG + REACH_TOL_DEG
                            and (plan["target"][j] - cmd[j]) * np.sign(d) < 0):
                        # 読んだ角度が古いと「読んだ角度 + 3°」が前の目標より後ろになる. 後ろへは戻さない (引き戻さない)
                        plan["target"][j] = cmd[j]
                        res["held_previous_target"] = True
                    res["clip"] = plan["clip"]
                    res["refused"] = plan["refused"]
                    res["limit_ja"] = limit_text_ja(plan)
                    res["limit_ascii"] = limit_text_ascii(plan)
                    if plan["target"] is None:
                        res["sent"] = False
                        return
                    res["command"] = _r(plan["target"])
                    res["_target"] = plan["target"]
                    res.update(self.arm.send_and_check(plan["target"], gate, self.quitting, check=False, joint=j))
                    return
                # 出発点: 直前の目標に届いていて, 今読んだ角度とも 3° 以内ならその目標.
                # そうでなければ今読んだ角度 (手で動かされたなど. 古い目標から大きく動かさない. codex 必須 3)
                gap = float(np.max(np.abs(np.asarray(snap["cmd"]) - q)))
                cmd0 = np.asarray(snap["cmd"], dtype=float)
                start = start_pose(cmd0, q)
                if not snap["cmd_reached"] or abs(float(cmd0[j] - q[j])) > REACH_TOL_DEG:
                    start[j] = q[j]                     # 動かす関節が届いていない・3° より離れていれば, 読んだ角度から
                res["start_source"] = "command" if start[j] == cmd0[j] else "measured"   # 動かす関節の出発点
                res["others_from_measured"] = [k for k in range(6) if k != j and start[k] != cmd0[k]]
                res["cmd_gap_deg"] = round(gap, 3)
                plan = plan_step(start, j, d)
                res["clip"] = plan["clip"]
                res["refused"] = plan["refused"]
                res["limit_ja"] = limit_text_ja(plan)
                res["limit_ascii"] = limit_text_ascii(plan)
                if plan["target"] is None:
                    res["sent"] = False
                    return
                res["command"] = _r(plan["target"])
                res["_target"] = plan["target"]
                res.update(self.arm.send_and_check(plan["target"], gate, self.quitting, joint=j))
            elif job[0] == "home":
                q = self.arm.read()
                res["measured_before"] = _r(q)
                if q is None:                # 読めなければ送らない (codex 必須 6)
                    res["error"] = "read_failed"
                    return
                res["command"] = _r(HOME)
                res["_target"] = np.array(HOME)
                res["range_check"] = "skipped_for_home"
                res.update(self.arm.send_and_check(np.array(HOME), gate, self.quitting))
            elif job[0] == "halt":
                res.update(self.arm.halt())
                q = res.get("measured_after_halt")
                hj = snap.get("halt_joint")
                if False and q is not None and hj is not None and not self.stopped:
                    # 11/5 16:13 にやめた: 重さで届かない j1 の指令を送ると, そのあとの指令が無視されやすくなった
                    # stop のあと, 動かしていない関節が 1〜2° 下がる (11/5 の記録で j1 +1°・j4 +1.5° 前後, 毎回).
                    # 止まった関節は止まった角度, ほかの関節は指令の角度で送り直して元に戻す.
                    # 戻さずに読んだ角度を次の出発点にすると, 止めるたびにずれが積み上がった (j1 が 5° → 40°)
                    pin = np.asarray(snap["cmd"], dtype=float).copy()
                    pin[hj] = float(q[hj])
                    lo, hi, _ls, _hs = joint_window()
                    if np.all(pin >= lo - LIMIT_EPS_DEG) and np.all(pin <= hi + LIMIT_EPS_DEG):
                        res["pin_command"] = _r(pin)
                        res["_pin"] = pin
                        r2 = self.arm.send_and_check(pin, gate, self.quitting, check=False)
                        res["pin_sent"] = r2.get("sent")
                    else:
                        res["pin_command"] = None
                        res["pin_refused"] = "out_of_window"
            elif job[0] == "grip":
                res["measured_before"] = None
                sent = self.arm.gripper(job[1], gate)
                res["sent"] = bool(sent)
                if not sent:
                    res["why"] = "halted_before_send"
        except Exception as ex:
            res["error"] = f"{type(ex).__name__}: {ex}"
        finally:
            self.results.put((job, res))

    def poll(self):
        """送る仕事の結果を受け取り, 状態とログに反映する (pygame のループから毎回呼ぶ)."""
        while True:
            try:
                job, res = self.results.get_nowait()
            except queue.Empty:
                return
            self._apply(job, res)

    def _apply(self, job, res):
        e = self.pending or self._entry(None, "orphan_result")
        self.pending = None
        self.busy = False
        target = res.pop("_target", None)
        e.update(res)
        if res.get("reached_angles") is not None:
            self.measured = np.asarray(res["reached_angles"], dtype=float)
        elif res.get("measured_before") is not None:
            self.measured = np.asarray(res["measured_before"], dtype=float)
        sent = res.get("sent")
        if job[0] == "halt":
            self._apply_halt(e, res)
            return
        if job[0] == "move" and sent is True and res.get("no_check") and e.get("hold_id") is not None:
            if self._hold_alive(e["hold_id"]):
                self.hold_last_sent_id = e["hold_id"]
                mb = e.get("measured_before")
                if res.get("hold_resend"):
                    if self.hold_sent is not None:
                        self.hold_sent["resends"] += 1
                        self.hold_sent["t"] = time.monotonic()
                        if mb is not None:
                            self.hold_sent["last_q"] = list(mb)
                elif mb is not None:
                    self.hold_sent = {"q": list(mb), "last_q": list(mb), "t": time.monotonic(), "resends": 0}
            else:
                # 送っている間に離された・止められた: 送った目標へ動き出しているので止める
                self.request_halt("released_while_sending", hold_id=e["hold_id"], joint=job[1])
        if (job[0] == "move" and res.get("why") == "lead_ok" and self.hold_sent is not None
                and e.get("measured_before") is not None):
            self.hold_sent["last_q"] = list(e["measured_before"])     # 次の確かめで「止まったか」を見るため
        if job[0] in ("move", "home") and sent is True:
            self.n_sent += 1
            self.cmd = np.asarray(target, dtype=float)
            # 長押しの送信は届くのを待たないが, アームは目標へ向かって動き続けるので「届く目標」として扱う
            # (次の短い押しは, 読んだ角度と 3° 以内ならこの目標から. 離れていれば読んだ角度から)
            # 1 刻みが送り直しても動かなかったとき (サーボが小さな動きに反応しない) も, 指令はその目標のままにする.
            # 次の押しはそこから 1 刻み先になるので, 押した分が積み上がって動き出す (3° 離れるまで. start_pose)
            self.cmd_reached = True if (res.get("no_check") or res.get("not_moved") or res.get("settled")) \
                else bool(res.get("reached"))
        elif sent == "unknown":
            self.cmd_reached = False                      # 次は読んだ角度から
        if job[0] == "grip" and res.get("error"):
            self.grip = "unknown"
        if job[0] == "grip" and sent is True:
            self.grip = "closed" if job[1] == 1 else "open"
        if job[0] == "move":
            self.last_limit = res.get("limit_ascii", "")
        e["reached"] = res.get("reached") if sent is True else None
        # ---- ターミナル ----
        if job[0] == "move":
            label = f"j{job[1]} {job[2]:+.1f}°" + (f"（長押し {e.get('hold_n')}）" if job[3] is not None else "")
        elif job[0] == "home":
            label = "HOME"
        else:
            label = "爪を" + ("閉じる" if job[1] == 1 else "開く")
        if res.get("limit_ja"):
            self.out(f"[範囲] {res['limit_ja']}")
        if res.get("error"):
            self.out(f"[エラー] {label}: {res['error']}。停止にします")
        elif sent is True and job[0] == "grip":
            self.out(f"[爪] {label}（待ち {GRIPPER_SLEEP:.1f} 秒のあと受付）")
        elif sent is True and res.get("no_check"):
            self._hold_progress(e, job)                    # 長押し: 1 回ずつは出さない (離したときに 1 行)
        elif res.get("why") == "lead_ok":
            self._hold_progress(e, job)                    # 目標へ向かっている途中 (送っていない). 進み具合だけ見る
        elif sent is True:
            r = res.get("reached")
            how = "届いた" if r else ("確かめる途中で終了" if r is None else
                                     ("動かなかった（次の押しは この目標の先から）" if res.get("not_moved") else
                                      ("手前で止まった（次の押しは この目標の先から）" if res.get("settled") else "届かなかった")))
            if res.get("resends"):
                how += f"・無視されたので {len(res['resends'])} 回送り直した"
            self.out(f"[送信] {label} → {_fmt(res.get('command'))}  {how}"
                     f"（ずれ {res.get('reach_err')}°）  届いた角度 {_fmt(res.get('reached_angles'))}")
        elif res.get("why") == "halted_before_send" and e.get("released_during_job") and not self.stopped:
            pass                                           # 長押しを離したので取り消した. 何も出さない
        elif res.get("why") == "halted_before_send":
            self.out(f"[送らない] {label}: 停止が入ったので送りませんでした")
        else:
            self.out(f"[送らない] {label}")
        # 長押しの中で送れなかったら (範囲の端・停止), その長押しは止める. 端で同じ行を繰り返さない
        if (job[0] == "move" and sent is not True and res.get("why") != "lead_ok"
                and e.get("hold_id") is not None and self._hold_alive(e["hold_id"])):
            self._end_hold(reason="hold_not_sent")
        self.gate = None
        self._finish(e)
        if res.get("error"):
            self.enter_stop(f"arm_error: {res['error']}", kind="arm_error")
        self._dispatch_halt()
        self._dispatch_step()

    def _apply_halt(self, e, res):
        pin = res.pop("_pin", None)
        e.pop("_pin", None)                                # _apply で先に e へ入っている (JSON にしない)
        e.update(res)
        q = res.get("measured_after_halt")
        if q is not None:
            self.measured = np.asarray(q, dtype=float)
        if pin is not None and res.get("pin_sent") is True:
            self.cmd = np.asarray(pin, dtype=float)       # 止まった関節は止まった角度, ほかは指令のまま
            self.cmd_reached = True
        elif q is not None and e.get("halt_joint") is not None:
            # 長押しを止めた: 止まった関節だけ止まった角度にする. ほかの関節の指令は変えない (重さで下がった分を拾わない)
            self.cmd[e["halt_joint"]] = float(q[e["halt_joint"]])
            self.cmd_reached = True
        elif q is not None:
            # 停止 (L2+R2) で HOME などの途中で止めた: 止まった所を指令にする (古い目標へ急に動かさない)
            self.cmd = np.asarray(q, dtype=float).copy()
            self.cmd_reached = True
        else:
            self.cmd_reached = False
        before = e.get("measured_at_request")
        if q is not None and before is not None:
            d = np.asarray(q, dtype=float) - np.asarray(before, dtype=float)
            e["moved_after_request_deg"] = _r(d)
        how = {"stop": "stop", "send_current": "今の角度へ送り直し（daemon に stop がない）",
               "dry": "ドライラン"}.get(res.get("halt"), "送れなかった")
        if res.get("halt_error"):
            self.out(f"[止める] 止める命令を送れませんでした: {res['halt_error']}（L2+R2 で停止・必要なら電源を切ってください）")
        elif e.get("halt_reason") not in ("release", "hold_end"):
            self.out(f"[止める] {how}（{e.get('halt_reason')}）  止まった角度 {_fmt(q)}")
        self.gate = None
        self._finish(e)
        self._dispatch_halt()
        self._dispatch_step()

    # ---- 終わり ----
    def shutdown(self):
        """新しい指令を送らずに終わる. 送っている途中の仕事が戻るのを WORKER_JOIN_SEC まで待つ.
        戻り値: 仕事が終わっているか (終わっていなければ daemon のソケットを閉じない)."""
        self.quitting.set()
        committed = self._cancel_gate()
        if self.pending is not None:
            self.pending["send_committed_before_exit"] = committed
        if self.thread is not None:
            self.thread.join(WORKER_JOIN_SEC)
        self.poll()
        done = self.thread is None or not self.thread.is_alive()
        if done and (self.halt_pending is not None or self.hold_last_sent_id is not None
                     or (self.hold is not None and self.hold_last_sent_id == self.hold["id"])):
            # 長押しの目標へ動いている途中で終わった: stop を 1 回だけ送る (新しく動かす指令ではない. レビュー 推奨 3)
            try:
                r = self.arm.halt()
            except Exception as ex:
                r = {"halt_error": f"{type(ex).__name__}: {ex}"}
            self.halt_pending = None
            self.hold_last_sent_id = None
            e = self._entry(None, "halt", accepted=True, halt_reason="exit")
            e.update(r)
            self._finish(e)
        if self.pending is not None:
            e = self.pending
            self.pending = None
            e.update(reached=None, interrupted_at_exit=True)
            self._finish(e)
        return done

    def status_lines(self, q_now=None):
        """画面の文字 (英数字だけ)."""
        q = self.measured if q_now is None else q_now
        lines = [
            ("big", f"JOINT  j{self.joint}    STEP {self.step:.1f} deg"),
            ("big", f"STATE  {self.state}" + ("  (moving)" if self.stopped and self.busy else "")),
            ("big", f"GRIP   {self.grip}"),
        ]
        for k in range(6):
            lines.append(("mono", f"j{k}  {q[k]:+8.2f}   cmd {self.cmd[k]:+8.2f}"))
        lines.append(("small", "last limit: " + (self.last_limit or "-")))
        if self.stopped:
            lines.append(("small", f"stop reason: {self.stop_reason}  (OPTIONS = resume)"))
        return lines


# =============================================================================
# pygame のループ
# =============================================================================

WINDOW_SIZE = (900, 560)


def controller_info(js):
    return {"name": js.get_name(), "guid": js.get_guid(), "instance_id": js.get_instance_id(),
            "axes": js.get_numaxes(), "buttons": js.get_numbuttons(), "hats": js.get_numhats(),
            "connected_at": datetime.now().isoformat(timespec="seconds")}


def run_loop(tp, screen, fonts, mode_text, on_controller=None, on_frame=None, finish_buttons=None):
    """ウィンドウを閉じるまで回す. 戻り値: 終わった理由.
    finish_buttons (run_policy_client の補正 p から. 2026-11-05 LOG450): このボタンを押したら, 止まっていれば
    "finish_button" で戻る (推論に戻る). 動いている間 (送っている・長押し・止める待ち) は断って続ける.
    このボタンは Teleop には渡さない."""
    js = None
    clock = pygame.time.Clock()
    frame = 0
    while True:
        if on_frame is not None:
            on_frame(frame, tp)
        frame += 1
        now = time.monotonic()
        events = pygame.event.get()
        buttons = []
        for ev in events:
            if ev.type == pygame.QUIT:
                return "window_closed"
            if ev.type == pygame.JOYDEVICEADDED:
                if js is None:
                    js = pygame.joystick.Joystick(ev.device_index)
                    info = controller_info(js)
                    tp.out(f"[接続] {info['name']}  GUID={info['guid']}  軸={info['axes']}  ボタン={info['buttons']}")
                    if on_controller is not None:
                        on_controller(info)
            elif ev.type == pygame.JOYDEVICEREMOVED:
                if js is not None and ev.instance_id == js.get_instance_id():
                    js = None
                    tp.controller_lost("controller_disconnected")
            elif ev.type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP):
                if js is not None and ev.instance_id == js.get_instance_id():
                    buttons.append(ev)
        # 停止 (L2+R2) と 読み取りの失敗を ボタンより先に見る (同じコマのボタンで送らない. codex 必須 5)
        read_ok = js is not None
        if js is not None:
            try:
                l2, r2 = js.get_axis(AXIS_L2), js.get_axis(AXIS_R2)
                tp.controller_read_ok()
                tp.set_triggers(l2, r2)
            except Exception as ex:          # pygame.error など
                read_ok = False
                tp.controller_read_failed(f"{type(ex).__name__}: {ex}")
        if finish_buttons and read_ok:
            for ev in buttons:
                if ev.type == pygame.JOYBUTTONDOWN and ev.button in finish_buttons:
                    if tp.busy or tp.hold is not None or tp.halt_pending is not None:
                        tp.out("[補正] 動いている間は終われません。止まってから □ を押してください")
                    else:
                        return "finish_button"
            buttons = [ev for ev in buttons if ev.button not in finish_buttons]
        if read_ok:
            for ev in buttons:
                if ev.type == pygame.JOYBUTTONDOWN:
                    tp.press(ev.button, now)
                else:
                    tp.release(ev.button, now)
        # 長押しの最中は, 離した合図を取りこぼしても止まるよう, ボタンの状態も毎コマ見る
        if js is not None and read_ok and tp.hold is not None:
            hb = tp.hold["button"]
            try:
                held = js.get_button(hb)
            except Exception as ex:          # pygame.error など
                tp.controller_read_failed(f"{type(ex).__name__}: {ex}")
            else:
                if not held:
                    tp.release(hb, now)
        tp.tick(now)
        tp.poll()
        draw(screen, fonts, tp, mode_text, js)
        clock.tick(60)


def draw(screen, fonts, tp, mode_text, js):
    screen.fill((20, 20, 24))
    color_state = {STATE_IDLE: (120, 220, 140), STATE_MOVING: (240, 200, 80),
                   STATE_STOPPED: (240, 90, 90)}[tp.state]
    y = 10
    screen.blit(fonts["small"].render(f"{mode_text}   controller: "
                                      + (js.get_name()[:40] if js is not None else "none"),
                                      True, (200, 220, 255)), (10, y))
    y += 28
    for kind, text in tp.status_lines():
        col = color_state if text.startswith("STATE") else (235, 235, 235)
        screen.blit(fonts[kind].render(text[:110], True, col), (10, y))
        y += {"big": 44, "mono": 26, "small": 24}[kind]
    y += 6
    for t in ("dpad L/R: j0   dpad U/D: selected joint (up = -, down = +)   hold: keep moving   triangle: next joint",
              "R1 hold: 2 deg   cross: close   circle: open   HOME hold 1s: home",
              "L2+R2: stop   OPTIONS: resume   window close / Ctrl+C: quit (arm is not moved)"):
        screen.blit(fonts["small"].render(t, True, (150, 150, 160)), (10, y))
        y += 22
    pygame.display.flip()


# =============================================================================
# main
# =============================================================================

def make_fonts():
    mono = "menlo,consolas,dejavusansmono"
    return {"big": pygame.font.SysFont(mono, 34, bold=True), "mono": pygame.font.SysFont(mono, 20),
            "small": pygame.font.SysFont(mono, 16)}


def run_embedded(arm, start, log, grip=None, mode_text="PAD CORRECTION", finish_buttons=(BTN_SQUARE,), out=print,
                 on_frame=None):
    """run_policy_client.py の推論のあとの確認で p を押したときに呼ぶ (2026-11-05 LOG450).
    このファイルの main と同じ Teleop と run_loop を, 推論のプロセスの中で動かす (daemon のつながりと鍵は
    推論と共有する. daemon は 1 本ずつしか受けないので, 別のプロセスでは動かせない).
    □ (finish_buttons) で推論に戻る. ウィンドウを閉じても戻る. 操作・送り方・守りは main と同じ.
    arm は DaemonArm (かその子) か DryArm. log は write(dict) / close() を持つもの.
    grip: 推論の側の爪の状態 (1 = 閉 / 0 = 開). 画面の GRIP に出すだけ.
    戻り値: (終わった理由, Teleop, 送る仕事が終わっているか)."""
    tp = Teleop(arm, start, log, out=out)
    if grip is not None:
        tp.grip = "closed" if int(grip) == 1 else "open"
    end_reason = None
    worker_done = True
    pygame.init()
    pygame.joystick.init()
    try:
        screen = pygame.display.set_mode(WINDOW_SIZE)
        pygame.display.set_caption(mode_text)
        out("コントローラーを待っています（つないでいなければ、今つないでください）。□ で推論に戻ります")
        end_reason = run_loop(tp, screen, make_fonts(), mode_text, finish_buttons=finish_buttons,
                              on_frame=on_frame)
    except KeyboardInterrupt:
        end_reason = "ctrl_c"
    finally:
        worker_done = tp.shutdown()
        pygame.quit()
    return end_reason, tp, worker_done


def git_info(path):
    def run(*a):
        try:
            return subprocess.run(["git", *a], cwd=_ROOT, capture_output=True, text=True,
                                  timeout=5).stdout.strip()
        except Exception:
            return None
    rel = os.path.relpath(path, _ROOT)
    return {"commit": run("rev-parse", "HEAD"),
            "script_status": run("status", "--porcelain", "--", rel) or "clean"}


def main(argv=None, on_frame=None, start_pose_dry=None):
    ap = argparse.ArgumentParser(description="ゲームパッドで mechArm を手で動かす (既定はドライラン)")
    ap.add_argument("--execute", action="store_true", help="daemon に送る (付けなければドライラン)")
    args = ap.parse_args(argv)

    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    run_stem = f"teleop_{stamp}" + ("" if args.execute else "_dry")
    if args.execute:
        ensure_ssd_mounted()
        try:
            load_daemon_modules()
        except Exception as ex:          # 読み込みの失敗・値の違い. 記録も daemon もまだ触っていない
            sys.exit(f"[ABORT] daemon の送り口を読めません: {type(ex).__name__}: {ex}。何も送らずに終わります")
        run_dir = LOG_ROOT / run_stem
        if run_dir.exists():
            sys.exit(f"[ABORT] 同じ名前のフォルダがあります: {run_dir}")
        run_dir.mkdir(parents=True)
        script_copy = run_dir / Path(__file__).name
        shutil.copy2(__file__, script_copy)
        log = JsonlLog(run_dir / "log.jsonl")
        print(f"[記録] {run_dir}")
    else:
        # ドライランは記録しない: KIOXIA に何も書かない・フォルダも作らない (依頼 69)
        run_dir = script_copy = None
        log = NullLog()
        print("[記録] ドライランなので記録しません")

    tp = None
    meta = None
    end_reason = None
    daemon_used = False
    try:
        if args.execute:
            get_daemon_socket()
            daemon_used = True
            rpc.install_daemon_lock()
            arm = DaemonArm()
            start = arm.read()
            if start is None:
                end_reason = "start_read_failed"
                sys.exit("[ABORT] 起動したときの関節角を読めません（daemon を確かめてください）。何も送らずに終わります")
            print("[実機] daemon に送ります。起動したときの姿勢: " + _fmt(start))
        else:
            start = np.asarray(start_pose_dry if start_pose_dry is not None else HOME, dtype=float)
            arm = DryArm(start)
            print("[ドライラン] daemon にもアームにも触りません。HOME から始まる仮の姿勢を動かします")
        lo, hi, _, _ = joint_window()
        bad = [k for k in range(6) if not (lo[k] - LIMIT_EPS_DEG <= start[k] <= hi[k] + LIMIT_EPS_DEG)]
        if bad:
            print("[注意] 起動したときの姿勢が守りの外: " + ", ".join(f"j{k}={start[k]:+.2f}" for k in bad)
                  + "。この関節を含む操作は送りません（HOME の長押しは送れます）")

        meta = None if not args.execute else {
            "run_stem": run_stem, "started_at": datetime.now().isoformat(timespec="seconds"),
            "execute": bool(args.execute), "argv": sys.argv,
            "controllers": [],
            "python": sys.version, "pygame": pygame.version.ver,
            "sdl": ".".join(str(v) for v in pygame.get_sdl_version()), "platform": platform.platform(),
            "start_pose": _r(start),
            "guards": {"J1_MAX_DEG": J1_MAX_DEG, "range": "physical_limits (no base window. 2026-11-03)",
                       "J4_SAFE": list(J4_SAFE), "JOINT_LIMITS": [list(x) for x in JOINT_LIMITS],
                       "window": {"lo": _r(lo), "hi": _r(hi)},
                       "home_bypasses_range_check": True},
            "motion": {"speed": SPEED, "step_fine_deg": STEP_FINE_DEG, "step_coarse_deg": STEP_COARSE_DEG,
                       "reach_tol_deg": REACH_TOL_DEG, "reach_timeout_min_sec": REACH_TIMEOUT_SEC,
                       "poll_sec": POLL_SLEEP, "gripper_sleep_sec": rpc.GRIPPER_SLEEP,
                       "grip_close_value": rpc.GRIP_VALUE, "send_wait": rpc.SEND_WAIT,
                       "home": HOME, "home_hold_sec": HOME_HOLD_SEC, "trigger_stop_above": TRIGGER_STOP_ABOVE,
                       "hold_start_sec": HOLD_START_SEC, "hold_interval_sec": HOLD_INTERVAL_SEC,
                       "hold_ahead_deg": HOLD_AHEAD_DEG, "hold_stall_sec": HOLD_STALL_SEC,
                       "ignored_check_sec": IGNORED_CHECK_SEC, "ignored_max_resends": IGNORED_MAX_RESENDS,
                       "j0_left_sign": J0_LEFT_SIGN, "up_sign": UP_SIGN, "select_order": list(SELECT_ORDER)},
            "button_map": {str(k): v for k, v in BUTTON_NAMES.items()},
            "git": git_info(__file__),
            "script_copy": script_copy.name,
            "script_sha256": hashlib.sha256(script_copy.read_bytes()).hexdigest(),
        }
        if meta is not None:
            write_json_atomic(run_dir / "meta.json", meta)

        def on_controller(info):
            if meta is None:
                return
            meta["controllers"].append(info)
            try:
                write_json_atomic(run_dir / "meta.json", meta)
            except Exception as ex:
                print(f"[記録エラー] meta.json に書けません: {ex}")

        tp = Teleop(arm, start, log)
        pygame.init()
        pygame.joystick.init()
        screen = pygame.display.set_mode(WINDOW_SIZE)
        pygame.display.set_caption("gamepad_teleop" + (" [EXECUTE]" if args.execute else " [DRY RUN]"))
        mono = "menlo,consolas,dejavusansmono"
        fonts = {"big": pygame.font.SysFont(mono, 34, bold=True), "mono": pygame.font.SysFont(mono, 20),
                 "small": pygame.font.SysFont(mono, 16)}
        print("コントローラーを待っています（つないでいなければ、今つないでください）")
        end_reason = run_loop(tp, screen, fonts, "EXECUTE" if args.execute else "DRY RUN",
                              on_controller=on_controller, on_frame=on_frame)
    except KeyboardInterrupt:
        end_reason = "ctrl_c"
        print("\n[終了] Ctrl+C")
    finally:
        # 終わるときにアームを動かさない (HOME 復帰もサーボ解放もしない). 新しい指令も送らない
        worker_done = True
        if tp is not None:
            worker_done = tp.shutdown()
            if not worker_done:
                print("[終了] 送っている途中の仕事が戻らないまま終わります（ソケットは閉じません）")
        if meta is not None:
            meta.update(ended_at=datetime.now().isoformat(timespec="seconds"), end_reason=end_reason or "error",
                        n_sent=(tp.n_sent if tp is not None else 0))
            try:
                write_json_atomic(run_dir / "meta.json", meta)
            except Exception as ex:
                print(f"[記録エラー] meta.json に書けません: {ex}")
        log.close()
        if daemon_used and worker_done:
            try:
                close_daemon_socket()
            except Exception:
                pass
        pygame.quit()
        print("[終了] アームは動かしていません。" + (f"記録: {run_dir}" if run_dir is not None else "記録なし（ドライラン）"))
    return 0


if __name__ == "__main__":
    sys.exit(main())
```

### pi05/test_pad_correction.py（テスト4件・commit cd2fb46）

#### pi05/test_pad_correction.py

```python
#!/usr/bin/env python3
"""test_pad_correction.py — run_policy_client.py の p (ゲームパッドで補正. 2026-11-05 LOG450) の確かめ.

本物の pygame のループ (SDL_VIDEODRIVER=dummy) に偽のコントローラーの押しを流し, 補正の結果の記録・
爪の記録・巻き戻し用の目標・ドライランの仮の姿勢・実機の送り口 (daemon を偽物にする) を見る.
  SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python3 pi05/test_pad_correction.py
"""
import functools
import io
import json
import os
import sys
import unittest
from pathlib import Path
from unittest import mock

os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))
sys.path.insert(0, str(_HERE.parent / "tools"))

import numpy as np  # noqa: E402

import run_policy_client as rpc  # noqa: E402
import gamepad_teleop as gt  # noqa: E402


class FakeJoystick:
    """test_gamepad_teleop.FakeJoystick と同じ (あちらは読み込むと刻みを 0.5° に書き換えるので写した)."""

    def __init__(self, idx):
        self.axes = [0.0, 0.0, 0.0, 0.0, -1.0, -1.0]

    def get_instance_id(self):
        return 7

    def get_name(self):
        return "PS4 Controller"

    def get_guid(self):
        return "fake-guid"

    def get_numaxes(self):
        return 6

    def get_numbuttons(self):
        return 16

    def get_numhats(self):
        return 0

    def get_axis(self, a):
        return self.axes[a]

    def get_button(self, b):
        return 0

HOME = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]


class FakeRecorder:
    def __init__(self):
        self.events = []

    def set_mode(self, m):
        self.events.append(("mode", m))

    def gripper(self, s):
        self.events.append(("gripper", s))


def script_frames(script):
    import pygame

    def btn(b, down=True):
        t = pygame.JOYBUTTONDOWN if down else pygame.JOYBUTTONUP
        pygame.event.post(pygame.event.Event(t, instance_id=7, button=b, joy=0))

    def on_frame(n, tp):
        if n == 1:
            pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0))
        for b in script.get(n, ()):
            btn(b); btn(b, False)
        if n == 600:
            pygame.event.post(pygame.event.Event(pygame.QUIT))
    return on_frame


LO = np.array([-60.0, -10, -10, -60, -60, -60])
HI = np.array([60.0, 60, 60, 60, 60, 0])


class TestPadDry(unittest.TestCase):
    def test_dry_flow(self):
        arm = rpc.ArmIO(False, None, sim_q=HOME)
        rec = FakeRecorder()
        log_f = io.StringIO()
        on_frame = script_frames({3: [gt.BTN_RIGHT], 40: [gt.BTN_RIGHT], 80: [gt.BTN_CROSS], 140: [gt.BTN_SQUARE]})
        orig = gt.run_embedded
        with mock.patch.object(gt, "run_embedded", functools.partial(orig, on_frame=on_frame)), \
                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
                mock.patch.object(gt, "GRIPPER_SLEEP", 0.0):
            res, grip = rpc.run_pad_correction(arm, 0, 1, 4, log_f, rec, LO, HI)
        self.assertEqual(res["action"], "accept")
        self.assertEqual(res["device"], "gamepad")
        self.assertEqual(res["pad_end_reason"], "finish_button")
        self.assertEqual(res["steps_sent"], 2)
        self.assertEqual(res["sent_targets"][-1][0], 2.0)
        self.assertTrue(res["grip_used"])
        self.assertEqual((res["grip_before"], res["grip_after"], grip), (0, 1, 1))
        self.assertAlmostEqual(arm.sim_q[0], 2.0)                    # 推論に戻る姿勢 = 直した姿勢
        self.assertEqual(rec.events[0], ("mode", rpc.MODE_CORRECT))
        self.assertEqual(rec.events[-1], ("mode", rpc.MODE_MODEL))
        rows = [json.loads(x) for x in log_f.getvalue().splitlines()]
        self.assertTrue(rows and all(r["type"] == "pad" and r["correction_id"] == 1 and r["after_chunk"] == 4
                                     for r in rows))
        self.assertGreaterEqual(res["duration_s"], 0.0)
        self.assertNotIn("outside_envelope", res)

    def test_outside_envelope_warns(self):
        arm = rpc.ArmIO(False, None, sim_q=[59.5, 0, 0, 0, 0, -30])
        on_frame = script_frames({3: [gt.BTN_RIGHT], 60: [gt.BTN_RIGHT], 120: [gt.BTN_SQUARE]})
        orig = gt.run_embedded
        out = []
        with mock.patch.object(gt, "run_embedded", functools.partial(orig, on_frame=on_frame)), \
                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
                mock.patch("builtins.print", lambda *a, **k: out.append(" ".join(map(str, a)))):
            res, _ = rpc.run_pad_correction(arm, 0, 2, 0, io.StringIO(), None, LO, HI)
        self.assertEqual(res["outside_envelope"], [0])
        self.assertTrue(any("学習データの範囲の外" in x for x in out))


class TestPadExecute(unittest.TestCase):
    """実機の送り口: daemon を偽物にして, 1 関節の send_angle と, 爪の指令の瞬間の記録を見る."""

    def test_execute_sends_and_records_gripper(self):
        q = {"now": np.array(HOME, dtype=float)}
        calls = []

        def req(c, **k):
            calls.append(c)
            if c["cmd"] == "send_angle":
                q["now"][c["joint_id"] - 1] = c["angle"]
                return {"ok": True}
            if c["cmd"] == "get_angles":
                return {"ok": True, "angles": list(q["now"])}
            return {"ok": True, "result": None}

        rec = FakeRecorder()
        arm = rpc.ArmIO(True, rec)
        on_frame = script_frames({3: [gt.BTN_LEFT], 60: [gt.BTN_CROSS], 120: [gt.BTN_SQUARE]})
        orig = gt.run_embedded
        gt.load_daemon_modules()
        self.assertIs(gt.rpc, sys.modules["run_policy_client"])
        with mock.patch.object(gt, "run_embedded", functools.partial(orig, on_frame=on_frame)), \
                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
                mock.patch.object(rpc, "read_angles", lambda: q["now"].copy()), \
                mock.patch.object(gt, "daemon_request", req), \
                mock.patch.object(rpc, "daemon_set_gripper_value", lambda v: calls.append({"cmd": "grip", "v": v})), \
                mock.patch.object(rpc, "GRIPPER_SLEEP", 0.0):
            res, grip = rpc.run_pad_correction(arm, 0, 3, 7, io.StringIO(), rec, LO, HI)
        self.assertEqual(res["pad_end_reason"], "finish_button")
        self.assertIn({"cmd": "send_angle", "joint_id": 1, "angle": -1.0, "speed": gt.SPEED, "wait": rpc.SEND_WAIT},
                      calls)
        self.assertIn({"cmd": "grip", "v": rpc.GRIP_VALUE}, calls)
        self.assertIn(("gripper", 1), rec.events)
        self.assertEqual(grip, 1)
        self.assertEqual(res["sent_targets"], [[-1.0, 0.0, 0.0, 0.0, 0.0, -30.0]])


class TestConfirmP(unittest.TestCase):
    """確認で p を押すと補正に入り, 終えたあとの b でその補正を巻き戻せる (アームなし --dry-keys)."""

    def test_p_then_b(self):
        import tempfile
        import time
        from test_rewind_correct import run_client

        def fake_pad(arm, grip, corr_id, chunk, log_f, recorder, lo, hi):
            start = arm.read()[:6].copy()
            t0 = time.time()
            tgt = start.copy(); tgt[0] += 3.0
            arm.sim_q = tgt.copy()
            return ({"type": "correction", "action": "accept", "device": "gamepad", "correction_id": corr_id,
                     "after_chunk": chunk, "t_start": t0, "t_end": time.time(), "start_pose": rpc._fmt6(start),
                     "final_target": rpc._fmt6(tgt), "measured_after": rpc._fmt6(tgt), "steps_sent": 1,
                     "grip_before": grip, "grip_after": grip, "grip_used": False, "regrip_used": False,
                     "sent_targets": [rpc._fmt6(tgt)]}, grip)
        td = Path(tempfile.mkdtemp())
        with mock.patch.object(rpc, "run_pad_correction", fake_pad):
            code, world, run, lines, out = run_client(
                rpc, ["--dry-keys", "--steps-per-chunk", "10", "--max-chunks", "4"], ["", "p", "b", "q"], td)
        self.assertEqual(code, 0)
        req = [x for x in lines if x.get("correction_requested")]
        self.assertEqual([x["correction_device"] for x in req], ["gamepad"])
        rw = [x for x in lines if x.get("type") == "rewind"]
        self.assertEqual([(x["kind"], x["correction_id"]) for x in rw], [("correction", 1)])
        self.assertIn("p=ゲームパッドで補正", rpc.CONFIRM_PROMPT)


if __name__ == "__main__":
    unittest.main()
```

### tools/test_gamepad_teleop.py（テスト90件・commit cd2fb46）

#### tools/test_gamepad_teleop.py

```python
#!/usr/bin/env python3
"""test_gamepad_teleop.py — gamepad_teleop.py の確かめ (アームなし・コントローラーなし. 依頼 68).

  python3 tools/test_gamepad_teleop.py -v

中身:
  - ボタン番号と操作の対応 (依頼 68 の表) / 十字キー上下の向き / △ で j1 → j5 → j1
  - 守り: j1 は 90.87 を超えない / 物理可動域まで (基準の姿勢からの枠はない. 2026-11-03) / J4_SAFE
  - 長押し (2026-11-03): 短い押しは 1 刻み / 押している間は送り続ける / 離す・停止・△・範囲の端で止まる /
    指令が読んだ角度より 3° 先なら送らずに待つ / 追いつかなければ止める
  - 停止: L2・R2 の両方 > 0.0 で止まる・片方では止まらない / 停止中は受け付けない / OPTIONS で戻る / 接続が切れたら停止
  - HOME: 1 秒に満たない長押しでは送らない / 1 秒続けたら 1 回だけ送る
  - 動いている間の押しは受け付けない (ためない)
  - daemon への送り方: 停止が立っていれば送らない / 届かなくても送り直さない
  - ドライランで main() がひととおり動く (偽のコントローラー・ダミーの画面・一時フォルダ)
  - 依頼 69: ドライランは KIOXIA に書かない (フォルダも作らない・KIOXIA がなくても動く) / --execute は今までどおり書く /
    ドライランでは cv2 (2 つ目の libSDL2) を読まない・objc の重複の警告が出ない (別のプロセスで確かめる)
  - codex R1 の指摘: 範囲外の出発点 / 目標と読んだ角度のずれ / 長押しの持ち越し / 停止と送信の順番 /
    再開で古い仕事が生き返らない / 読み取り失敗 / 記録の失敗 / ボタンより先に L2+R2 を見る
KIOXIA にもアームにも触らない.
"""
import json
import os
import subprocess
import sys
import tempfile
import threading
import types
import time
import unittest
from pathlib import Path
from unittest import mock

os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")

import numpy as np  # noqa: E402

sys.path.insert(0, str(Path(__file__).resolve().parent))
import gamepad_teleop as gt  # noqa: E402

# DaemonArm のテストで daemon の送り口 (の差し替え) を使うので読み込んでおく.
# ドライランで読まないことは TestNoCv2InDryRun で別のプロセスで確かめる
gt.load_daemon_modules()

HOME = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]

# テストは 0.5° の刻みで書いてある. 実際の値 (1°. 2026-11-03) は TestStepValue で確かめる
STEP_FINE_REAL = gt.STEP_FINE_DEG
gt.STEP_FINE_DEG = 0.5


class TestStepValue(unittest.TestCase):
    def test_real_steps(self):
        self.assertEqual(STEP_FINE_REAL, 1.0)
        self.assertEqual(gt.STEP_COARSE_DEG, 2.0)


class ListLog:
    def __init__(self):
        self.rows = []

    def write(self, rec):
        json.dumps(rec, ensure_ascii=False)      # JSON にできること
        self.rows.append(rec)


class FakeArm:
    execute = False

    def __init__(self, start=HOME, reach=True):
        self.q = np.asarray(start, dtype=float).copy()
        self.sent = []
        self.grips = []
        self.reach = reach

    def read(self):
        return self.q.copy()

    def send_and_check(self, target, gate, quitting, check=True, joint=None):
        if not gate.run(lambda: self.sent.append([float(v) for v in target])):
            return {"sent": False, "why": "halted_before_send"}
        if not check:
            self.checks = getattr(self, "checks", []) + [False]
            self.q = np.asarray(target, dtype=float).copy() if self.reach else self.q
            return {"sent": True, "reached": None, "no_check": True}
        if self.reach:
            self.q = np.asarray(target, dtype=float).copy()
        return {"sent": True, "reached": self.reach, "reached_angles": gt._r(self.q), "reach_err": 0.0}

    def gripper(self, state, gate):
        return gate.run(lambda: self.grips.append(state))

    def halt(self):
        self.halts = getattr(self, "halts", 0) + 1
        return {"halt": "stop", "measured_after_halt": gt._r(self.q)}


def make(start=HOME, reach=True, sync=True):
    arm = FakeArm(start, reach)
    log = ListLog()
    spawned = []
    spawn = (lambda fn: fn()) if sync else spawned.append
    tp = gt.Teleop(arm, start, log, out=lambda *a: None, spawn=spawn)
    return tp, arm, log, spawned


def press(tp, b, now=0.0):
    tp.press(b, now)
    tp.release(b, now)
    tp.poll()


class TestButtonMap(unittest.TestCase):
    def test_numbers(self):
        self.assertEqual((gt.BTN_CROSS, gt.BTN_CIRCLE, gt.BTN_SQUARE, gt.BTN_TRIANGLE), (0, 1, 2, 3))
        self.assertEqual((gt.BTN_SHARE, gt.BTN_HOME, gt.BTN_OPTIONS, gt.BTN_L3, gt.BTN_R3), (4, 5, 6, 7, 8))
        self.assertEqual((gt.BTN_L1, gt.BTN_R1), (9, 10))
        self.assertEqual((gt.BTN_UP, gt.BTN_DOWN, gt.BTN_LEFT, gt.BTN_RIGHT, gt.BTN_TOUCHPAD), (11, 12, 13, 14, 15))
        self.assertEqual((gt.AXIS_L2, gt.AXIS_R2), (4, 5))

    def test_dpad_left_right_always_j0(self):
        for sel in range(5):
            tp, arm, log, _ = make()
            tp.sel = sel
            press(tp, gt.BTN_RIGHT)
            self.assertEqual(log.rows[-1]["joint"], 0)
            self.assertAlmostEqual(arm.sent[-1][0], 0.5)
            press(tp, gt.BTN_LEFT)
            press(tp, gt.BTN_LEFT)
            self.assertAlmostEqual(arm.sent[-1][0], -0.5)
            self.assertEqual(arm.sent[-1][1:], HOME[1:])

    def test_up_minus_down_plus_starts_j1(self):
        tp, arm, log, _ = make()
        self.assertEqual(tp.joint, 1)
        press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][1], 0.5)
        press(tp, gt.BTN_UP)
        press(tp, gt.BTN_UP)
        self.assertAlmostEqual(arm.sent[-1][1], -0.5)

    def test_triangle_cycles(self):
        tp, arm, log, _ = make()
        seen = [tp.joint]
        for _ in range(5):
            press(tp, gt.BTN_TRIANGLE)
            seen.append(tp.joint)
        self.assertEqual(seen, [1, 2, 3, 4, 5, 1])
        self.assertEqual(arm.sent, [])                      # 切り替えだけでは送らない
        tp.sel = 3                                           # j4
        press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][4], 0.5)

    def test_r1_coarse_step(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_R1, 0.0)
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 2.0)
        press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][1], 2.0)
        tp.release(gt.BTN_R1, 0.0)
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 2.5)

    def test_gripper(self):
        tp, arm, log, _ = make()
        press(tp, gt.BTN_CROSS)
        self.assertEqual(arm.grips, [1])
        self.assertEqual(tp.grip, "closed")
        press(tp, gt.BTN_CIRCLE)
        self.assertEqual(arm.grips, [1, 0])
        self.assertEqual(tp.grip, "open")

    def test_unassigned_do_nothing(self):
        tp, arm, log, _ = make()
        for b in (gt.BTN_SQUARE, gt.BTN_SHARE, gt.BTN_L1, gt.BTN_L3, gt.BTN_R3, gt.BTN_TOUCHPAD):
            press(tp, b)
        self.assertEqual(arm.sent, [])
        self.assertEqual(arm.grips, [])
        self.assertTrue(all(r["kind"] == "ignored" for r in log.rows))
        self.assertEqual(tp.joint, 1)


class TestGuards(unittest.TestCase):
    def test_j1_never_above_9087(self):
        start = [0.0, 85.0, 0.0, 0.0, 0.0, -30.0]
        tp, arm, log, _ = make(start)
        tp.r1 = True
        for _ in range(6):
            press(tp, gt.BTN_DOWN)
        self.assertTrue(all(s[1] <= 90.87 + 1e-9 for s in arm.sent))
        self.assertAlmostEqual(arm.sent[-1][1], 90.87)
        clipped = [r for r in log.rows if r.get("clip")]
        self.assertEqual(clipped[0]["clip"][0]["limit"], "J1_MAX")
        refused = [r for r in log.rows if r.get("refused")]
        self.assertTrue(refused and refused[-1]["refused"][0]["why"] == "at_limit")
        self.assertIn("J1_MAX", tp.last_limit)

    def test_home_to_pick_height(self):
        # HOME から j1 を PICK の高さ 90.87 まで下ろせる (基準の姿勢からの ±10° の枠はない. 2026-11-03)
        tp, arm, log, _ = make()
        tp.r1 = True
        for _ in range(50):
            press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(max(s[1] for s in arm.sent), 90.87)
        self.assertFalse(any(r.get("clip") and r["clip"][0]["limit"] == "BASE" for r in log.rows))
        # ほかの関節も ±10° を超えて動く
        for _ in range(12):
            press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 24.0)
        tp.sel = 1                                          # j2
        for _ in range(15):
            press(tp, gt.BTN_UP)
        self.assertAlmostEqual(arm.sent[-1][2], -30.0)
        tp.sel = 4                                          # j5
        for _ in range(10):
            press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][5], -10.0)

    def test_j4_safe(self):
        start = [0.0, 0.0, 0.0, 0.0, 55.0, -30.0]
        tp, arm, log, _ = make(start)
        tp.sel = 3
        tp.r1 = True
        for _ in range(5):
            press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(max(s[4] for s in arm.sent), 60.0)
        self.assertEqual([r for r in log.rows if r.get("clip")][0]["clip"][0]["limit"], "J4_SAFE")

    def test_window_function(self):
        lo, hi, lo_src, hi_src = gt.joint_window()
        self.assertAlmostEqual(hi[1], 90.87)
        self.assertEqual(hi_src[1], "J1_MAX")
        self.assertAlmostEqual(lo[4], -90.0)
        self.assertAlmostEqual(hi[4], 60.0)
        self.assertEqual((lo_src[4], hi_src[4]), ("J4_SAFE", "J4_SAFE"))
        for k in (0, 2, 3, 5):                                # 残りは物理可動域
            self.assertEqual((lo[k], hi[k]), gt.JOINT_LIMITS[k])
            self.assertEqual((lo_src[k], hi_src[k]), ("PHYS", "PHYS"))
        self.assertEqual(lo[1], gt.JOINT_LIMITS[1][0])

    def test_no_compensation_other_joints(self):
        start = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]
        p = gt.plan_step(start, 1, 20.0)
        self.assertAlmostEqual(p["target"][1], 20.0)
        self.assertEqual(list(p["target"][[0, 2, 3, 4, 5]]), [0.0, 0.0, 0.0, 0.0, -30.0])

    def test_out_of_window_start_refuses(self):
        # 起動したときに j1 が 91 (守りの外): j1 を下げる (−) 押しも, 他の関節の押しも送らない
        start = [0.0, 91.0, 0.0, 0.0, 0.0, -30.0]
        p = gt.plan_step(start, 0, 0.5)
        self.assertIsNone(p["target"])
        self.assertEqual(p["refused"][0]["joint"], 1)

    def test_home_then_continue(self):
        start = [10.0, 5.0, 0.0, 0.0, 0.0, -30.0]
        tp, arm, log, _ = make(start)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.01)
        tp.poll()
        self.assertEqual(arm.sent[-1], HOME)
        tp.r1 = True
        for _ in range(10):
            press(tp, gt.BTN_LEFT)
        self.assertAlmostEqual(arm.sent[-1][0], -20.0)       # HOME から続けて動く (枠はない)

    def test_home_bypasses_range(self):
        # 範囲の外 (j1=95) にいても HOME は送れる
        start = [0.0, 95.0, 0.0, 0.0, 0.0, -30.0]
        tp, arm, log, _ = make(start)
        press(tp, gt.BTN_DOWN)
        self.assertEqual(arm.sent, [])
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.0)
        tp.poll()
        self.assertEqual(arm.sent, [HOME])

    def test_start_from_measured_when_not_reached(self):
        tp, arm, log, _ = make(reach=False)
        press(tp, gt.BTN_RIGHT)                    # 届かない (仮の姿勢は HOME のまま)
        self.assertFalse(tp.cmd_reached)
        press(tp, gt.BTN_RIGHT)                    # 次は読んだ角度 (0) から
        self.assertAlmostEqual(arm.sent[-1][0], 0.5)
        self.assertEqual(log.rows[-1]["start_source"], "measured")


class LagArm(FakeArm):
    """送ると「届いた」と返すが, 角度は読むたびに lag_step ずつしか追いつかない (読みが古いのと同じ)."""

    def __init__(self, start=HOME, lag_step=0.0):
        super().__init__(start)
        self.target = np.asarray(start, dtype=float).copy()
        self.lag_step = lag_step
        self.reads = 0

    def read(self):
        self.reads += 1
        d = self.target - self.q
        self.q = self.q + np.clip(d, -self.lag_step, self.lag_step)
        return self.q.copy()

    def send_and_check(self, target, gate, quitting, check=True, joint=None):
        if not gate.run(lambda: self.sent.append([float(v) for v in target])):
            return {"sent": False, "why": "halted_before_send"}
        self.target = np.asarray(target, dtype=float).copy()
        if not check:
            return {"sent": True, "reached": None, "no_check": True}
        return {"sent": True, "reached": True, "reached_angles": gt._r(self.q), "reach_err": 0.0}

    def halt(self):
        self.halts = getattr(self, "halts", 0) + 1
        self.target = self.q.copy()                   # stop: その場で止まる
        return {"halt": "stop", "measured_after_halt": gt._r(self.q)}


def make_arm(arm, sync=True):
    log = ListLog()
    out = []
    tp = gt.Teleop(arm, arm.q.copy(), log, out=out.append, spawn=lambda fn: fn())
    return tp, log, out


class TestHold(unittest.TestCase):
    """長押し (2026-11-05 作り直し): 0.4 秒で「読んだ角度 + 15° 先」を 1 回送り, 0.1 秒おきに読んで,
    残り 5° 以下になったら次の 15° 先を送る. 届くのを待たない. 長押しが終わったら stop を送る."""

    def test_short_press_is_one_step(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_RIGHT, 0.0)
        tp.poll()
        tp.release(gt.BTN_RIGHT, 0.1)
        for t in (0.4, 0.6, 1.0):
            tp.tick(t)
            tp.poll()
        self.assertEqual(len(arm.sent), 1)

    def test_hold_sends_ahead_of_measured_without_waiting(self):
        arm = LagArm(lag_step=1.0)                    # 読むたびに 1° ずつ目標へ寄る
        tp, log, out = make_arm(arm)
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()        # 押した瞬間には送らない (11/5)
        for t in (0.1, 0.3, 0.39):
            tp.tick(t); tp.poll()
        self.assertEqual(len(arm.sent), 0)
        t = 0.4
        for _ in range(30):
            tp.tick(t); tp.poll()
            t += 0.1
        rows = [r for r in log.rows if r["kind"] == "move" and r.get("hold_n", 0) >= 1]
        sends = [r for r in rows if r.get("sent") is True]
        waits = [r for r in rows if r.get("why") == "lead_ok"]
        self.assertGreaterEqual(len(sends), 2)        # 30 回読んで, 送ったのは数回だけ
        self.assertLessEqual(len(sends), 4)
        self.assertEqual(len(arm.sent), len(sends))   # 1 刻みは送らない (長押しだけ)
        self.assertEqual(len(sends) + len(waits), 30)
        for r in sends:                               # 動かす関節は 読んだ角度 + 15°, ほかは そのまま
            self.assertAlmostEqual(r["command"][0], r["measured_before"][0] + gt.HOLD_AHEAD_DEG, places=2)
            self.assertEqual(r["command"][1:], HOME[1:])
            self.assertTrue(r["no_check"])
        for r in waits:                               # 送らなかったのは, 残りが 5° より多いとき
            self.assertGreater(r["hold_remaining_deg"], gt.HOLD_REFILL_DEG)
        for r in sends[1:]:                           # 次を送ったのは, 残りが 5° 以下になってから
            self.assertLessEqual(r["hold_lead_deg"], gt.HOLD_REFILL_DEG + 1e-9)
        n = len(arm.sent)
        tp.release(gt.BTN_RIGHT, t)
        tp.poll()
        for t2 in (t + 0.2, t + 0.5):
            tp.tick(t2); tp.poll()
        self.assertEqual(len(arm.sent), n)            # 離したら stop だけ (送り直しはしない. 11/5 16:13)
        self.assertEqual(arm.halts, 1)                # 離したら stop を 1 回送る
        self.assertEqual(arm.sent[-1][1:], HOME[1:])
        halt = [r for r in log.rows if r["kind"] == "halt"]
        self.assertEqual(len(halt), 1)
        self.assertEqual(halt[0]["halt_reason"], "release")
        self.assertTrue(any("[長押し] j0" in o for o in out))     # 離したときに 1 行
        q = arm.q.copy()
        for _ in range(5):
            arm.read()
        self.assertTrue(np.allclose(arm.q, q))        # stop のあと動かない
        self.assertTrue(np.allclose(tp.cmd, q))       # 止まった所が次の出発点

    def test_hold_keeps_other_joints_at_command(self):
        # j1 が重さで 1° 沈んでいても, 長押しの送信で j1 を読んだ値で送り直さない (ずり下がらない)
        arm = LagArm(start=[0, 30.0, 0, 0, 0, -30], lag_step=1.0)
        tp, log, out = make_arm(arm)
        arm.q[1] = 31.0
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        t = 0.4
        for _ in range(5):
            tp.tick(t); tp.poll()
            t += 0.1
        self.assertTrue(all(abs(s[1] - 30.0) < 1e-9 for s in arm.sent))

    def test_hold_direction_and_selected_joint(self):
        tp, arm, log, _ = make()
        tp.sel = 1                                    # j2
        tp.press(gt.BTN_UP, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()
        self.assertAlmostEqual(arm.sent[-1][2], -gt.HOLD_AHEAD_DEG)

    def test_triangle_and_other_buttons_end_hold(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_DOWN, 0.0); tp.poll()
        tp.press(gt.BTN_TRIANGLE, 0.1)
        tp.tick(0.5); tp.poll()
        self.assertEqual(len(arm.sent), 0)
        tp.press(gt.BTN_RIGHT, 1.0); tp.poll()
        tp.press(gt.BTN_CIRCLE, 1.1); tp.poll()       # 爪で止まる
        tp.tick(1.5); tp.poll()
        self.assertEqual(len(arm.sent), 0)
        self.assertEqual(arm.grips, [0])

    def test_stop_ends_hold_and_resume_does_not_revive(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()
        tp.set_triggers(0.5, 0.5)
        tp.tick(0.6); tp.poll()
        self.assertEqual(len(arm.sent), 1)
        self.assertIsNone(tp.hold)
        tp.set_triggers(-1.0, -1.0)
        press(tp, gt.BTN_OPTIONS, 0.7)
        tp.tick(0.9); tp.tick(1.2); tp.poll()
        self.assertEqual(len(arm.sent), 1)

    def test_disconnect_ends_hold(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.controller_lost("controller_disconnected")
        tp.tick(0.5); tp.poll()
        self.assertEqual(len(arm.sent), 0)
        self.assertIsNone(tp.hold)

    def test_limit_ends_hold_without_repeating_lines(self):
        start = [0.0, 80.0, 0.0, 0.0, 0.0, -30.0]
        arm = LagArm(start=start, lag_step=0.5)
        tp, log, out = make_arm(arm)
        tp.press(gt.BTN_DOWN, 0.0); tp.poll()         # 80 → 80.5
        tp.tick(0.4); tp.poll()                       # 読んだ角度 + 15 → 90.87 に切る
        self.assertAlmostEqual(arm.sent[-1][1], 90.87)
        t = 0.5
        for _ in range(60):                           # 端へ向かっている間は送らず, 端の手前 3° 以内で長押しを止める
            if tp.hold is None:
                break
            tp.tick(t); tp.poll()
            t += 0.1
        self.assertIsNone(tp.hold)
        self.assertGreater(arm.q[1], 90.87 - gt.REACH_TOL_DEG - 0.3)   # 端の 5° 手前で止めない
        n_rows = len(log.rows)
        for t2 in (t + 0.1, t + 0.5, t + 2.0):
            tp.tick(t2); tp.poll()
        self.assertEqual(len(arm.sent), 1)            # 長押しの 1 回だけ
        self.assertEqual(len(log.rows), n_rows)
        refused = [r for r in log.rows if r.get("refused")]
        self.assertEqual(refused[-1]["refused"][0]["why"], "at_limit")
        self.assertTrue(any(r.get("at_limit_target") for r in log.rows))
        self.assertEqual(arm.halts, 1)

    def test_hold_target_never_goes_back(self):
        # 読みが古く, 前の目標より後ろの「読んだ角度 + 3°」になるときは, 前の目標を送る (引き戻さない)
        arm = LagArm(lag_step=2.0)
        tp, log, out = make_arm(arm)
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()
        arm.lag_step = 0.0                            # 読みが進まない (古い)
        arm.q[0] = 0.0
        tp.tick(0.5); tp.poll()
        js = [s[0] for s in arm.sent]
        self.assertEqual(js, sorted(js))              # 右へ押している間, 目標は戻らない
        self.assertEqual(log.rows[-1].get("why"), "lead_ok")   # 目標へ向かっている途中なので 送らない

    def test_stall_ends_hold(self):
        arm = LagArm(lag_step=0.0)                    # まったく動かない (当たっている)
        tp, log, out = make_arm(arm)
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        t = 0.4
        for _ in range(20):
            tp.tick(t); tp.poll()
            t += 0.1
        self.assertIsNone(tp.hold)
        self.assertLessEqual(len(arm.sent), 1 + 12)
        self.assertTrue(any("進まないので止めました" in o for o in out))
        self.assertTrue(all(s[0] <= gt.HOLD_AHEAD_DEG + 1e-9 for s in arm.sent))   # 先へ出るのは 15° まで
        self.assertLessEqual(len(arm.sent), 2 + gt.HOLD_MAX_RESENDS)   # 1 刻み + 長押しの 1 回 + 送り直し (3 回まで)
        self.assertEqual(arm.halts, 1)                # 止めたときに stop を送る

    def test_short_press_after_hold_starts_from_command(self):
        tp, arm, log, _ = make()                      # FakeArm は目標へすぐ着く
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()
        tp.release(gt.BTN_RIGHT, 0.45)
        tp.poll()                                     # 離したときの stop が終わる
        last = arm.sent[-1][0]
        press(tp, gt.BTN_RIGHT, 1.0)
        self.assertAlmostEqual(arm.sent[-1][0], last + 0.5)
        self.assertEqual(log.rows[-1]["start_source"], "command")


class TestHalt(unittest.TestCase):
    """止める命令 (2026-11-05): 長押しの終わり・停止で daemon の stop を送る."""

    def test_stop_during_home_sends_halt(self):
        tp, arm, log, spawned = make([5.0, 0, 0, 0, 0, -30], sync=False)
        tp.press(gt.BTN_HOME, 0.0); tp.tick(1.0)
        spawned.pop()(); tp.poll()                 # HOME を送った (FakeArm はすぐ着くが, 実機は動いている途中)
        tp.set_triggers(1.0, 1.0)
        self.assertEqual(len(spawned), 1)          # 停止中でも stop は送る
        spawned.pop()(); tp.poll()
        self.assertEqual(arm.halts, 1)
        self.assertTrue(tp.stopped)

    def test_halt_waits_for_running_job_and_is_not_cancelled(self):
        tp, arm, log, spawned = make(sync=False)
        tp.press(gt.BTN_RIGHT, 0.0)
        tp.tick(0.4)                               # 長押しの 1 回目 (まだ走らせていない)
        job = spawned.pop()
        job(); tp.poll()                           # 送った
        tp.tick(0.5)                               # 読むだけの仕事
        check = spawned.pop()
        tp.release(gt.BTN_RIGHT, 0.55)             # 離した: 走っている仕事があるので stop は待つ
        self.assertEqual(spawned, [])
        check(); tp.poll()
        self.assertEqual(len(spawned), 1)          # 仕事が終わってから stop
        tp.set_triggers(1.0, 1.0)                  # stop を送る前に停止しても, stop は取り消さない
        spawned.pop()(); tp.poll()
        self.assertGreaterEqual(arm.halts, 1)

    def test_halt_pins_other_joints_no_drift(self):
        # stop のあと j1 が 1° 沈む (11/5 の実機). 止めるたびに 読んだ角度を出発点にすると積み上がった
        arm = LagArm(start=[0, 30.0, 0, 0, 0, -30], lag_step=5.0)
        tp, log, out = make_arm(arm)
        orig_halt = arm.halt
        def sag_halt():
            arm.q[1] += 1.0
            arm.target[1] += 1.0
            return orig_halt()
        arm.halt = sag_halt
        t = 0.0
        for _ in range(5):                            # 長押しを 5 回
            tp.press(gt.BTN_RIGHT, t); tp.poll()
            for k in range(4):
                tp.tick(t + 0.4 + 0.1 * k); tp.poll()
            tp.release(gt.BTN_RIGHT, t + 0.8); tp.poll()
            t += 2.0
        self.assertAlmostEqual(tp.cmd[1], 30.0)       # 指令の j1 は動かない
        self.assertTrue(all(abs(s_[1] - 30.0) < 1e-9 for s_ in arm.sent))
        halts = [r for r in log.rows if r["kind"] == "halt"]
        self.assertEqual(len(halts), 5)
        self.assertTrue(all("pin_sent" not in r for r in halts))   # 止めたあとは何も送らない

    def test_daemon_arm_step_ignores_other_joints_sag(self):
        # j1 が重さで 5° 下がっていても, 動かした j0 が着けば「届いた」 (11/5)
        saved = {k: getattr(gt, k) for k in ("daemon_send_angles", "rpc", "wr", "SPEED", "POLL_SLEEP", "REACH_TIMEOUT_SEC",
                                             "daemon_request")}
        try:
            gt.daemon_request = lambda c, **k: {"ok": True}
            gt.rpc = types.SimpleNamespace(SEND_WAIT=0, read_angles=lambda: np.array([1.0, 45.0, 0, 0, 0, -30]))
            gt.wr = types.SimpleNamespace(_compute_arrival_timeout=lambda d: 8.0)
            gt.daemon_send_angles = lambda a, **k: None
            gt.SPEED, gt.POLL_SLEEP, gt.REACH_TIMEOUT_SEC = 20, 0.0, 8.0
            r = gt.DaemonArm().send_and_check(np.array([1.0, 40.0, 0, 0, 0, -30]), gt.SendGate(), threading.Event(), joint=0)
            self.assertTrue(r["reached"])
        finally:
            for k, v in saved.items():
                setattr(gt, k, v)

    def test_daemon_arm_step_settles_short_without_waiting(self):
        # j2 が重さで 1.6° 手前で止まる: 8 秒待たずに「手前で止まった」で戻る (11/5)
        saved = {k: getattr(gt, k) for k in ("daemon_send_angles", "rpc", "wr", "SPEED", "POLL_SLEEP", "REACH_TIMEOUT_SEC",
                                             "daemon_request")}
        try:
            seq = iter([0.0, -0.3, -0.6, -0.6, -0.6, -0.6, -0.6, -0.6, -0.6, -0.6, -0.6, -0.6] + [-0.6] * 200)
            gt.rpc = types.SimpleNamespace(SEND_WAIT=0, read_angles=lambda: np.array([0.0, 0, next(seq), 0, 0, -30]))
            gt.wr = types.SimpleNamespace(_compute_arrival_timeout=lambda d: 8.0)
            gt.daemon_request = lambda c, **k: {"ok": True}
            gt.SPEED, gt.POLL_SLEEP, gt.REACH_TIMEOUT_SEC = 20, 0.3, 8.0
            t0 = time.monotonic()
            r = gt.DaemonArm().send_and_check(np.array([0.0, 0, -2.0, 0, 0, -30]), gt.SendGate(), threading.Event(), joint=2)
            self.assertLess(time.monotonic() - t0, 1.0)
            self.assertTrue(r["settled"])
            self.assertEqual(r["resends"], [])
        finally:
            for k, v in saved.items():
                setattr(gt, k, v)

    def test_hold_resends_when_stopped_short(self):
        # 次の目標が無視されて前の目標の手前で止まった: 0.3 秒たったら同じ目標を送り直す (11/5)
        arm = LagArm(lag_step=0.0)
        tp, log, out = make_arm(arm)
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()                       # 15° 先を送った (でも動かない)
        tp.hold_sent["t"] -= 1.0                      # 送ってから 1 秒たったことにする
        tp.tick(0.5); tp.poll()
        self.assertEqual(log.rows[-1].get("hold_resend"), True)
        self.assertEqual(arm.sent[-1], arm.sent[-2])  # 同じ目標
        self.assertEqual(tp.hold_sent["resends"], 1)

    def test_daemon_arm_step_not_moved_returns_and_resends(self):
        saved = {k: getattr(gt, k) for k in ("daemon_send_angles", "rpc", "wr", "SPEED", "POLL_SLEEP", "REACH_TIMEOUT_SEC",
                                             "IGNORED_CHECK_SEC", "daemon_request")}
        try:
            sends = []
            gt.rpc = types.SimpleNamespace(SEND_WAIT=0, read_angles=lambda: np.array([0.0, 0, 0, 0, 0, -30]))
            gt.wr = types.SimpleNamespace(_compute_arrival_timeout=lambda d: 8.0)
            gt.daemon_send_angles = lambda a, **k: self.fail("1 刻みで 6 関節を送った")
            gt.daemon_request = lambda c, **k: (sends.append(c) or {"ok": True})
            gt.SPEED, gt.POLL_SLEEP, gt.REACH_TIMEOUT_SEC, gt.IGNORED_CHECK_SEC = 20, 0.0, 8.0, 0.01
            t0 = time.monotonic()
            r = gt.DaemonArm().send_and_check(np.array([1.0, 0, 0, 0, 0, -30]), gt.SendGate(), threading.Event(), joint=0)
            self.assertLess(time.monotonic() - t0, 1.0)  # 8 秒待たない
            self.assertFalse(r["reached"])              # 1° 動かなければ「届いた」にしない (3° の判定では届いたになっていた)
            self.assertTrue(r["not_moved"])
            self.assertEqual(len(sends), 1 + gt.STEP_MAX_RESENDS)
            self.assertEqual({(c["cmd"], c["joint_id"], c["angle"]) for c in sends}, {("send_angle", 1, 1.0)})
        finally:
            for k, v in saved.items():
                setattr(gt, k, v)

    def test_not_moved_step_accumulates(self):
        tp, arm, log, _ = make()
        def nm(target, gate, quitting, check=True, joint=None):
            gate.run(lambda: arm.sent.append([float(v) for v in target]))
            return {"sent": True, "reached": False, "not_moved": True, "reached_angles": gt._r(arm.q)}
        arm.send_and_check = nm
        press(tp, gt.BTN_RIGHT)
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 1.0)  # 動かなくても 押した分は積み上がる (0.5 → 1.0)

    def test_short_press_does_not_send_halt(self):
        tp, arm, log, _ = make()
        press(tp, gt.BTN_RIGHT)
        tp.tick(0.5); tp.poll()
        self.assertEqual(getattr(arm, "halts", 0), 0)

    def test_halt_error_does_not_stop_or_loop(self):
        tp, arm, log, _ = make()
        arm.halt = lambda: {"halt_error": "boom", "measured_after_halt": None}
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()
        tp.release(gt.BTN_RIGHT, 0.5); tp.poll()
        self.assertFalse(tp.stopped)
        self.assertFalse(tp.busy)
        self.assertEqual(sum(r["kind"] == "halt" for r in log.rows), 1)

    def test_daemon_arm_halt_falls_back_without_stop_cmd(self):
        calls = []
        saved = {k: getattr(gt, k) for k in ("daemon_request", "daemon_send_angles", "rpc", "SPEED", "HALT_SETTLE_SEC")}
        try:
            gt.daemon_request = lambda c, timeout_override=None: (calls.append(("req", c)) or {"ok": False, "error": "unknown cmd: stop"})
            gt.daemon_send_angles = lambda a, **k: calls.append(("send", a))
            gt.rpc = types.SimpleNamespace(SEND_WAIT=0, read_angles=lambda: np.array([1.0, 2, 3, 4, 5, -30]))
            gt.SPEED = 20
            gt.HALT_SETTLE_SEC = 0.0
            r = gt.DaemonArm().halt()
            self.assertEqual(r["halt"], "send_current")
            self.assertEqual(calls[0], ("req", {"cmd": "stop"}))
            self.assertEqual(calls[1], ("send", [1.0, 2.0, 3.0, 4.0, 5.0, -30.0]))
            gt.daemon_request = lambda c, timeout_override=None: {"ok": True, "result": {"jog_stop": 0, "stop": 0}}
            calls.clear()
            r = gt.DaemonArm().halt()
            self.assertEqual(r["halt"], "stop")
            self.assertEqual(calls, [])
        finally:
            for k, v in saved.items():
                setattr(gt, k, v)

    def test_daemon_arm_check_returns_when_stopped(self):
        # HOME の途中で L2+R2: 届くのを待たずに戻る (戻れば すぐ stop を送れる. レビュー 必須 1)
        saved = {k: getattr(gt, k) for k in ("daemon_send_angles", "rpc", "wr", "SPEED", "POLL_SLEEP", "REACH_TIMEOUT_SEC")}
        try:
            gate = gt.SendGate()
            reads = []
            def read_angles():
                reads.append(1)
                if len(reads) == 3:
                    gate.cancel_now()                  # 動いている途中で停止
                return np.array([20.0, 0, 0, 0, 0, -30]) - len(reads)
            gt.rpc = types.SimpleNamespace(SEND_WAIT=0, read_angles=read_angles)
            gt.wr = types.SimpleNamespace(_compute_arrival_timeout=lambda d: 8.0)
            gt.daemon_send_angles = lambda a, **k: None
            gt.SPEED, gt.POLL_SLEEP, gt.REACH_TIMEOUT_SEC = 20, 0.0, 8.0
            t0 = time.monotonic()
            r = gt.DaemonArm().send_and_check(np.array(HOME), gate, threading.Event())
            self.assertLess(time.monotonic() - t0, 1.0)
            self.assertTrue(r["halted_during_check"])
            self.assertLessEqual(len(reads), 4)
        finally:
            for k, v in saved.items():
                setattr(gt, k, v)

    def test_arm_error_also_sends_halt(self):
        tp, arm, log, _ = make()
        def bad(target, gate, quitting, check=True, joint=None):
            gate.run(lambda: None)
            return {"sent": "unknown", "error": "send_failed: timeout"}
        arm.send_and_check = bad
        press(tp, gt.BTN_RIGHT); tp.poll()
        self.assertTrue(tp.stopped)
        self.assertEqual(arm.halts, 1)
        self.assertEqual(sum(r["kind"] == "halt" for r in log.rows), 1)   # 失敗しても繰り返さない

    def test_exit_during_hold_sends_halt_once(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        tp.tick(0.4); tp.poll()                       # 長押しの目標を送った
        self.assertTrue(tp.shutdown())
        self.assertEqual(arm.halts, 1)
        self.assertEqual([r for r in log.rows if r["kind"] == "halt"][-1]["halt_reason"], "exit")

    def test_exit_when_idle_sends_nothing(self):
        tp, arm, log, _ = make()
        press(tp, gt.BTN_RIGHT)
        self.assertTrue(tp.shutdown())
        self.assertEqual(getattr(arm, "halts", 0), 0)

    def test_dry_arm_hold_moves_on_read_and_halt_stops(self):
        arm = gt.DryArm()
        tp, log, out = make_arm(arm)
        tp.press(gt.BTN_RIGHT, 0.0); tp.poll()
        t = 0.4
        for _ in range(20):
            tp.tick(t); tp.poll()
            t += 0.1
        self.assertGreater(arm.q[0], 15.0)            # 読むたびに寄っていく
        tp.release(gt.BTN_RIGHT, t); tp.poll()
        q = arm.read().copy()
        self.assertTrue(np.allclose(arm.read(), q))


class TestHoldReview(unittest.TestCase):
    """2026-11-03 のレビューの指摘 (A: 受け付けられなかった押し / B: 離したあとの 1 刻み / D: 離した合図の取りこぼし)."""

    def test_press_while_busy_starts_hold_when_free(self):
        # 11/5: 動いている間に押して押し続けていれば, 空いてから長押しになる (向きを変えたとき受付なしにしない)
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_CROSS)                       # 爪の仕事で busy
        self.assertTrue(tp.busy)
        tp.press(gt.BTN_DOWN, 0.0)
        self.assertIsNotNone(tp.hold)
        tp.tick(0.4)                                  # まだ空いていない: 送らない
        self.assertEqual(len(spawned), 1)
        spawned.pop(0)()                              # 爪の仕事が終わる
        tp.poll()
        for t in (0.6, 0.8, 1.0):
            tp.tick(t)
            while spawned:
                spawned.pop(0)()
            tp.poll()
        self.assertGreaterEqual(len(arm.sent), 1)
        self.assertAlmostEqual(arm.sent[0][1], gt.HOLD_AHEAD_DEG)

    def test_press_rejected_while_stopped_does_not_become_hold(self):
        tp, arm, log, _ = make()
        tp.set_triggers(0.5, 0.5)
        tp.press(gt.BTN_RIGHT, 0.0)
        self.assertIsNone(tp.hold)

    def test_release_cancels_uncommitted_repeat(self):
        tp, arm, log, spawned = make(sync=False)
        out = []
        tp.out = out.append
        tp.press(gt.BTN_RIGHT, 0.0)
        tp.tick(0.4)                                  # 長押しの 1 回目の仕事ができた (まだ走っていない)
        tp.release(gt.BTN_RIGHT, 0.45)                # 走る前に離した
        spawned.pop(0)(); tp.poll()
        self.assertEqual(len(arm.sent), 0)
        last = [r for r in log.rows if r["kind"] == "move"][-1]
        self.assertEqual(last["why"], "halted_before_send")
        self.assertTrue(last["released_during_job"])
        self.assertFalse(tp.stopped)
        self.assertFalse(any("停止が入った" in o for o in out))
        tp.press(gt.BTN_RIGHT, 1.0)                   # 次の押しは普通に動く
        tp.release(gt.BTN_RIGHT, 1.05)
        spawned.pop(0)(); tp.poll()
        self.assertEqual(len(arm.sent), 1)

    def test_release_does_not_cancel_first_step(self):
        tp, arm, log, spawned = make(sync=False)
        tp.press(gt.BTN_RIGHT, 0.0)
        tp.release(gt.BTN_RIGHT, 0.05)                # 短い押し: 仕事が走る前に離しても 1 刻みは送る
        spawned.pop(0)(); tp.poll()
        self.assertEqual(len(arm.sent), 1)

    def test_loop_releases_hold_when_button_state_is_up(self):
        import pygame
        pygame.init()
        screen = pygame.display.set_mode((200, 100))
        fonts = {k: pygame.font.SysFont("menlo", 12) for k in ("big", "mono", "small")}
        js = FakeJoystick(0)
        js.held = set()
        tp, arm, log, _ = make()
        frames = {
            0: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            2: lambda: (js.held.add(gt.BTN_RIGHT),
                        pygame.event.post(pygame.event.Event(pygame.JOYBUTTONDOWN, instance_id=7, button=gt.BTN_RIGHT, joy=0))),
            4: lambda: js.held.discard(gt.BTN_RIGHT),   # 離した合図 (JOYBUTTONUP) は来ない
            60: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }
        seen = {}

        def on_frame(n, t):
            frames.get(n, lambda: None)()
            if n == 3:
                seen["hold_at_3"] = t.hold is not None
        with mock.patch.object(gt.pygame.joystick, "Joystick", lambda i: js):
            gt.run_loop(tp, screen, fonts, "TEST", on_frame=on_frame)
        pygame.quit()
        self.assertTrue(seen["hold_at_3"])            # 押している間は長押しのまま
        self.assertIsNone(tp.hold)                    # 状態が離れたら止まる
        self.assertEqual(len(arm.sent), 1)            # 1 秒近くたっても 1 刻みだけ


class TestCodexR1(unittest.TestCase):
    def test_selected_joint_out_of_window_not_clamped(self):
        # 基準 j1=100 (守りの外) から −0.5 を押しても 90.87 へ 1 回で動かさない
        start = [0.0, 100.0, 0.0, 0.0, 0.0, -30.0]
        p = gt.plan_step(start, 1, -0.5)
        self.assertIsNone(p["target"])
        self.assertEqual(p["refused"][0]["why"], "start_out_of_window")
        tp, arm, log, _ = make(start)
        press(tp, gt.BTN_UP)
        self.assertEqual(arm.sent, [])

    def test_physical_limit_ends(self):
        base = [0.0, 0.0, 170.0, 0.0, 0.0, -30.0]       # j2 の物理可動域は ±177
        p = gt.plan_step(base, 2, 20.0)
        self.assertAlmostEqual(p["target"][2], 177.0)
        self.assertEqual(p["clip"][0]["limit"], "PHYS")

    def test_gap_between_command_and_measured(self):
        tp, arm, log, _ = make()
        press(tp, gt.BTN_RIGHT)                         # 目標 j0=0.5 に届いた
        arm.q[0] = 10.0                                 # 手で動かされた
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 10.5)   # 今読んだ角度から 0.5
        self.assertEqual(log.rows[-1]["start_source"], "measured")
        arm.q[0] = -170.0                               # 物理可動域 (-156) の外 → 送らない
        press(tp, gt.BTN_RIGHT)
        self.assertEqual(len(arm.sent), 2)

    def test_home_hold_across_busy_is_dropped(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)                         # 動いている
        tp.press(gt.BTN_HOME, 0.0)                      # busy 中に押し始め
        spawned.pop()()
        tp.poll()                                       # 0.5 秒で空く
        tp.tick(1.5)
        self.assertEqual(spawned, [])
        self.assertIn(("home", False, "busy"), [(r["kind"], r.get("accepted"), r.get("reason")) for r in log.rows])

    def test_home_hold_dropped_when_other_op_starts(self):
        tp, arm, log, spawned = make(sync=False)
        tp.press(gt.BTN_HOME, 0.0)
        press(tp, gt.BTN_RIGHT)                         # 長押しの途中で j0 を動かした
        tp.tick(0.2)
        spawned.pop()()
        tp.poll()
        tp.tick(1.5)
        self.assertEqual(spawned, [])
        self.assertIn("hold_cancelled", [r.get("reason") for r in log.rows])

    def test_home_hold_dropped_by_stop_and_disconnect(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_HOME, 0.0)
        tp.set_triggers(1, 1)
        tp.set_triggers(-1, -1)
        press(tp, gt.BTN_OPTIONS)
        tp.tick(1.5)
        self.assertEqual(arm.sent, [])
        tp.r1 = True
        tp.press(gt.BTN_HOME, 2.0)
        tp.controller_lost("controller_disconnected")
        self.assertFalse(tp.r1)
        press(tp, gt.BTN_OPTIONS)
        tp.tick(5.0)
        self.assertEqual(arm.sent, [])

    def test_resume_does_not_revive_old_job(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        tp.set_triggers(1, 1)
        tp.set_triggers(-1, -1)
        press(tp, gt.BTN_OPTIONS)                       # すぐ再開
        self.assertFalse(tp.stopped)
        spawned.pop()()                                 # 止める前の仕事が走る
        tp.poll()
        self.assertEqual(arm.sent, [])

    def test_home_read_failure_does_not_send(self):
        tp, arm, log, _ = make([5.0, 0, 0, 0, 0, -30])
        reads = iter([None, np.array(HOME)])
        arm.read = lambda: next(reads)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.0)
        tp.poll()
        self.assertEqual(arm.sent, [])
        self.assertTrue(tp.stopped)

    def test_log_failure_blocks_resume(self):
        tp, arm, log, _ = make()

        def bad(rec):
            raise OSError("disk gone")
        log.write = bad
        press(tp, gt.BTN_TRIANGLE)
        self.assertTrue(tp.stopped)
        press(tp, gt.BTN_OPTIONS)
        self.assertTrue(tp.stopped)
        press(tp, gt.BTN_RIGHT)
        self.assertEqual(arm.sent, [])

    def test_loop_reads_triggers_before_buttons(self):
        import pygame
        pygame.init()
        screen = pygame.display.set_mode((200, 100))
        fonts = {k: pygame.font.SysFont("menlo", 12) for k in ("big", "mono", "small")}
        js = FakeJoystick(0)
        tp, arm, log, _ = make()
        frames = {
            0: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            2: lambda: (js.axes.__setitem__(slice(4, 6), [1.0, 1.0]),
                        pygame.event.post(pygame.event.Event(pygame.JOYBUTTONDOWN, instance_id=7, button=gt.BTN_RIGHT, joy=0))),
            4: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }
        with mock.patch.object(gt.pygame.joystick, "Joystick", lambda i: js):
            gt.run_loop(tp, screen, fonts, "TEST", on_frame=lambda n, t: frames.get(n, lambda: None)())
        self.assertEqual(arm.sent, [])
        self.assertTrue(tp.stopped)
        # 読み取りに失敗したコマのボタンも処理しない
        js2 = FakeJoystick(0)
        js2.get_axis = lambda a: (_ for _ in ()).throw(pygame.error("gone"))
        tp2, arm2, log2, _ = make()
        frames2 = {
            0: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            2: lambda: pygame.event.post(pygame.event.Event(pygame.JOYBUTTONDOWN, instance_id=7, button=gt.BTN_RIGHT, joy=0)),
            4: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }
        with mock.patch.object(gt.pygame.joystick, "Joystick", lambda i: js2):
            gt.run_loop(tp2, screen, fonts, "TEST", on_frame=lambda n, t: frames2.get(n, lambda: None)())
        self.assertEqual(arm2.sent, [])
        self.assertTrue(tp2.stopped)
        pygame.quit()


class TestShutdown(unittest.TestCase):
    def test_uncommitted_job_is_not_sent_at_exit(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        tp.shutdown()                                # 仕事はまだ走っていない
        spawned.pop()()                              # 終了のあとに走っても送らない
        self.assertEqual(arm.sent, [])
        last = [r for r in log.rows if r["kind"] == "move"][0]
        self.assertIs(last["send_committed_before_exit"], False)
        self.assertTrue(last["interrupted_at_exit"])

    def test_committed_send_is_waited_and_recorded(self):
        tp, arm, log, _ = make(sync=False)
        release = threading.Event()
        entered = threading.Event()
        orig = arm.send_and_check

        def slow(target, gate, quitting, check=True, joint=None):
            def fn():
                entered.set()
                release.wait(2.0)
                arm.sent.append([float(v) for v in target])
            if not gate.run(fn):
                return {"sent": False, "why": "halted_before_send"}
            return {"sent": True, "reached": None, "interrupted": True}
        arm.send_and_check = slow
        tp.spawn = tp._spawn_thread
        press(tp, gt.BTN_RIGHT)
        entered.wait(1.0)
        threading.Timer(0.2, release.set).start()
        done = tp.shutdown()                          # 送ると決めた送信が戻るまで待つ
        self.assertTrue(done)
        self.assertEqual(len(arm.sent), 1)
        move = [r for r in log.rows if r["kind"] == "move"][0]
        self.assertIs(move["send_committed_before_exit"], True)

    def test_worker_not_done_returns_false(self):
        tp, arm, log, _ = make(sync=False)
        entered = threading.Event()

        def stuck(target, gate, quitting, check=True, joint=None):
            gate.run(lambda: (entered.set(), threading.Event().wait(1.0)))
            return {"sent": True}
        arm.send_and_check = stuck
        tp.spawn = tp._spawn_thread
        press(tp, gt.BTN_RIGHT)
        entered.wait(1.0)
        with mock.patch.object(gt, "WORKER_JOIN_SEC", 0.1):
            self.assertFalse(tp.shutdown())           # 戻らない → main はソケットを閉じない


class TestStop(unittest.TestCase):
    def test_both_triggers(self):
        tp, arm, log, _ = make()
        tp.set_triggers(0.5, -1.0)
        self.assertFalse(tp.stopped)
        tp.set_triggers(-1.0, 0.9)
        self.assertFalse(tp.stopped)
        tp.set_triggers(0.0, 0.0)                  # ちょうど 0.0 は超えていない
        self.assertFalse(tp.stopped)
        tp.set_triggers(0.01, 0.01)
        self.assertTrue(tp.stopped)
        self.assertEqual(tp.state, gt.STATE_STOPPED)

    def test_stopped_rejects_and_options_resumes(self):
        tp, arm, log, _ = make()
        tp.set_triggers(1.0, 1.0)
        for b in (gt.BTN_RIGHT, gt.BTN_DOWN, gt.BTN_CROSS, gt.BTN_CIRCLE):
            press(tp, b)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(2.0)
        tp.release(gt.BTN_HOME, 2.0)
        self.assertEqual(arm.sent, [])
        self.assertEqual(arm.grips, [])
        self.assertTrue(all(r.get("reason") == "stopped" for r in log.rows
                            if r["kind"] in ("move", "grip_close", "grip_open", "home")))
        press(tp, gt.BTN_OPTIONS)                  # まだ L2+R2 を押している → 戻らない
        self.assertTrue(tp.stopped)
        tp.set_triggers(-1.0, -1.0)
        press(tp, gt.BTN_OPTIONS)
        self.assertFalse(tp.stopped)
        press(tp, gt.BTN_RIGHT)
        self.assertEqual(len(arm.sent), 1)

    def test_disconnect_and_read_error(self):
        tp, arm, log, _ = make()
        tp.controller_lost("controller_disconnected")
        self.assertTrue(tp.stopped)
        self.assertEqual(log.rows[-1]["kind"], "controller_lost")
        tp2, arm2, log2, _ = make()
        tp2.controller_read_failed("x")
        tp2.controller_read_failed("x")
        self.assertTrue(tp2.stopped)
        self.assertEqual(sum(r["kind"] == "read_error" for r in log2.rows), 1)
        press(tp2, gt.BTN_OPTIONS)                 # 読めないうちは戻らない
        self.assertTrue(tp2.stopped)
        tp2.controller_read_ok()
        press(tp2, gt.BTN_OPTIONS)
        self.assertFalse(tp2.stopped)

    def test_stop_while_moving_blocks_send(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)                    # 仕事を作ったが, まだ走らせていない
        self.assertTrue(tp.busy)
        tp.set_triggers(1.0, 1.0)                  # 動いている間でも停止は受け付ける
        self.assertTrue(tp.stopped)
        spawned.pop()()                            # 仕事が走る: 送る直前に停止を見て送らない
        tp.poll()
        self.assertEqual(arm.sent, [])
        move = [r for r in log.rows if r["kind"] == "move"][0]
        self.assertEqual(move["why"], "halted_before_send")
        self.assertTrue(tp.busy)                   # 停止の stop を送る仕事 (前の仕事が終わってから)
        spawned.pop()()
        tp.poll()
        self.assertFalse(tp.busy)
        self.assertEqual(arm.halts, 1)
        self.assertEqual([r for r in log.rows if r["kind"] == "halt"][0]["halt_reason"], "L2+R2")


class TestHomeHold(unittest.TestCase):
    def test_short_press_does_not_send(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_HOME, 10.0)
        tp.tick(10.5)
        tp.tick(10.99)
        tp.release(gt.BTN_HOME, 10.99)
        tp.poll()
        self.assertEqual(arm.sent, [])
        self.assertEqual(log.rows[-1]["kind"], "home_short")

    def test_hold_sends_once(self):
        tp, arm, log, _ = make([5.0, 0, 0, 0, 0, -30])
        tp.press(gt.BTN_HOME, 0.0)
        for t in (0.5, 1.0, 1.5, 3.0, 5.0):
            tp.tick(t)
            tp.poll()
        tp.release(gt.BTN_HOME, 5.0)
        self.assertEqual(arm.sent, [HOME])
        self.assertEqual(sum(r["kind"] == "home" for r in log.rows), 1)
        self.assertFalse(any(r["kind"] == "home_short" for r in log.rows))
        tp.press(gt.BTN_HOME, 6.0)                 # 押し直せば, また 1 回
        tp.tick(7.0)
        tp.poll()
        self.assertEqual(len(arm.sent), 2)


class TestBusy(unittest.TestCase):
    def test_presses_while_moving_are_dropped(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        for b in (gt.BTN_RIGHT, gt.BTN_DOWN, gt.BTN_CROSS):
            press(tp, b)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.5)
        self.assertEqual(len(spawned), 1)           # 仕事は 1 つだけ
        spawned.pop()()
        tp.poll()
        self.assertEqual(len(spawned), 1)           # 最後の十字キー (下) だけ 終わってから送る
        spawned.pop()()
        tp.poll()
        tp.tick(3.0)                                # 長押しのまま空いても, ためた HOME は出ない
        self.assertEqual(spawned, [])
        # 動いている間の十字キーは, 最後の 1 つだけ 終わってから送る (11/5). 爪と HOME は受付なし
        self.assertEqual(arm.sent[0], [0.5, 0, 0, 0, 0, -30])
        busy = [r for r in log.rows if r.get("reason") == "busy"]
        self.assertEqual(len(busy), 2)


class TestDaemonArm(unittest.TestCase):
    def _arm(self, reads):
        it = iter(reads)
        last = [None]

        def read():
            try:
                last[0] = next(it)
            except StopIteration:
                pass
            return None if last[0] is None else np.asarray(last[0], dtype=float)
        return read

    def test_halt_blocks_send(self):
        sends = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda *a, **k: sends.append(a)):
            gate, quit_ = gt.SendGate(), threading.Event()
            gate.cancel_now()
            r = gt.DaemonArm().send_and_check(np.array(HOME), gate, quit_)
        self.assertEqual(sends, [])
        self.assertFalse(r["sent"])

    def test_single_send_no_retry(self):
        sends = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: sends.append((a, k))), \
                mock.patch.object(gt, "REACH_TIMEOUT_SEC", 0.3), \
                mock.patch.object(gt.wr, "_compute_arrival_timeout", lambda d: 0.1), \
                mock.patch.object(gt, "POLL_SLEEP", 0.05):
            r = gt.DaemonArm().send_and_check(np.array([5.0, 0, 0, 0, 0, -30]),
                                              gt.SendGate(), threading.Event())
        self.assertEqual(len(sends), 1)
        self.assertEqual(sends[0][1]["speed"], 20)
        self.assertFalse(r["reached"])
        self.assertTrue(r["sent"])

    def test_ignored_send_is_resent(self):
        # 送っても動かない (無視された) → 0.5 秒後に同じ目標を送り直す → 動いて届く (2026-11-03)
        sends = []
        state = {"q": np.array(HOME, dtype=float)}

        def send(a, **k):
            sends.append(list(a))
            if len(sends) >= 2:                          # 2 回目で通る
                state["q"] = np.array(a, dtype=float)

        target = [5.0, 0, 0, 0, 0, -30]
        with mock.patch.object(gt.rpc, "read_angles", lambda: state["q"].copy()), \
                mock.patch.object(gt, "daemon_send_angles", send), \
                mock.patch.object(gt, "IGNORED_CHECK_SEC", 0.05), \
                mock.patch.object(gt, "POLL_SLEEP", 0.02):
            r = gt.DaemonArm().send_and_check(np.array(target), gt.SendGate(), threading.Event())
        self.assertEqual(len(sends), 2)
        self.assertEqual(sends[1], target)
        self.assertTrue(r["reached"])
        self.assertEqual(len(r["resends"]), 1)

    def test_resend_limited_and_not_when_moving(self):
        sends = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: sends.append(a)), \
                mock.patch.object(gt, "IGNORED_CHECK_SEC", 0.02), \
                mock.patch.object(gt, "REACH_TIMEOUT_SEC", 0.5), \
                mock.patch.object(gt.wr, "_compute_arrival_timeout", lambda d: 0.1), \
                mock.patch.object(gt, "POLL_SLEEP", 0.01):
            r = gt.DaemonArm().send_and_check(np.array([5.0, 0, 0, 0, 0, -30]), gt.SendGate(), threading.Event())
        self.assertEqual(len(sends), 1 + gt.IGNORED_MAX_RESENDS)
        self.assertFalse(r["reached"])
        # 動いている (届いていないが 0.3° 以上動いた) なら送り直さない
        sends.clear()
        moving = [HOME] + [[0.5 * (i + 1), 0, 0, 0, 0, -30] for i in range(6)]
        with mock.patch.object(gt.rpc, "read_angles", self._arm(moving)), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: sends.append(a)), \
                mock.patch.object(gt, "IGNORED_CHECK_SEC", 0.0), \
                mock.patch.object(gt, "REACH_TIMEOUT_SEC", 0.2), \
                mock.patch.object(gt.wr, "_compute_arrival_timeout", lambda d: 0.1), \
                mock.patch.object(gt, "POLL_SLEEP", 0.01):
            gt.DaemonArm().send_and_check(np.array([9.0, 0, 0, 0, 0, -30]), gt.SendGate(), threading.Event())
        self.assertEqual(len(sends), 1)

    def test_no_resend_after_stop(self):
        sends = []
        gate = gt.SendGate()
        reads = {"n": 0}

        def read():
            reads["n"] += 1
            if reads["n"] == 2:
                gate.cancel_now()                         # 確かめる途中で L2+R2
            return np.array(HOME, dtype=float)
        with mock.patch.object(gt.rpc, "read_angles", read), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: sends.append(a)), \
                mock.patch.object(gt, "IGNORED_CHECK_SEC", 0.0), \
                mock.patch.object(gt, "POLL_SLEEP", 0.01):
            r = gt.DaemonArm().send_and_check(np.array([5.0, 0, 0, 0, 0, -30]), gate, threading.Event())
        self.assertEqual(len(sends), 1)
        self.assertTrue(r["halted_during_check"])

    def test_reached(self):
        target = [1.0, 0, 0, 0, 0, -30]
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME, [0.5, 0, 0, 0, 0, -30]])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: None):
            r = gt.DaemonArm().send_and_check(np.array(target), gt.SendGate(), threading.Event())
        self.assertTrue(r["reached"])
        self.assertEqual(r["reach_err"], 0.5)

    def test_reach_uses_unrounded_error(self):
        target = [3.004, 0, 0, 0, 0, -30]
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: None), \
                mock.patch.object(gt, "REACH_TIMEOUT_SEC", 0.2), \
                mock.patch.object(gt.wr, "_compute_arrival_timeout", lambda d: 0.1), \
                mock.patch.object(gt, "POLL_SLEEP", 0.05):
            r = gt.DaemonArm().send_and_check(np.array(target), gt.SendGate(), threading.Event())
        self.assertFalse(r["reached"])

    def test_read_none_during_check_is_error(self):
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME, None])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: None):
            r = gt.DaemonArm().send_and_check(np.array([5.0, 0, 0, 0, 0, -30]), gt.SendGate(), threading.Event())
        self.assertTrue(r["sent"])
        self.assertEqual(r["error"], "read_failed_during_check")
        self.assertIsNone(r["reached"])

    def test_stop_does_not_wait_and_reports_commit(self):
        # 送ると決めて送信 (通信) の最中に停止が来ても, 停止は待たない. 決めていたことを返す.
        # 停止のあとに「送ると決める」ことはない
        gate = gt.SendGate()
        entered, release = threading.Event(), threading.Event()

        def slow_send():
            entered.set()
            release.wait(2.0)
        t = threading.Thread(target=lambda: gate.run(slow_send))
        t.start()
        entered.wait(1.0)
        got = []
        stopper = threading.Thread(target=lambda: got.append(gate.cancel_now()))
        stopper.start()
        stopper.join(0.5)
        self.assertFalse(stopper.is_alive())         # 待たされない
        self.assertEqual(got, [True])                # 停止より前に送ると決めていた
        release.set()
        t.join(2.0)
        self.assertFalse(gate.run(lambda: self.fail("取り消しのあとに送った")))
        g2 = gt.SendGate()
        self.assertFalse(g2.cancel_now())            # まだ決めていない仕事は送らない
        self.assertFalse(g2.run(lambda: self.fail("取り消しのあとに送った")))

    def test_send_uses_rpc_timeout(self):
        seen = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: seen.append(k)):
            gt.DaemonArm().send_and_check(np.array(HOME), gt.SendGate(), threading.Event())
        self.assertEqual(seen[0]["timeout_override"], gt.SEND_RPC_TIMEOUT_SEC)

    def test_stop_log_says_whether_send_was_committed(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        tp.set_triggers(1, 1)                       # まだ送ると決めていない
        stop = [r for r in log.rows if r["kind"] == "stop"][0]
        self.assertIs(stop["send_committed_before_stop"], False)

    def test_gripper_gate(self):
        calls = []
        with mock.patch.object(gt, "daemon_set_gripper_value", lambda v: calls.append(("close", v))), \
                mock.patch.object(gt, "daemon_set_gripper", lambda s, wait: calls.append(("open", s, wait))), \
                mock.patch.object(gt.rpc, "GRIPPER_SLEEP", 0.0):
            arm = gt.DaemonArm()
            self.assertTrue(arm.gripper(1, gt.SendGate()))
            self.assertTrue(arm.gripper(0, gt.SendGate()))
            g = gt.SendGate()
            g.cancel_now()
            self.assertFalse(arm.gripper(1, g))
        self.assertEqual(calls, [("close", 20), ("open", 0, gt.rpc.GRIP_OPEN_DAEMON_WAIT)])


class FakeJoystick:
    def __init__(self, idx):
        self.axes = [0.0, 0.0, 0.0, 0.0, -1.0, -1.0]

    def get_instance_id(self):
        return 7

    def get_name(self):
        return "PS4 Controller"

    def get_guid(self):
        return "fake-guid"

    def get_numaxes(self):
        return 6

    def get_numbuttons(self):
        return 16

    def get_numhats(self):
        return 0

    def get_axis(self, a):
        return self.axes[a]

    def get_button(self, b):
        return 1 if b in getattr(self, "held", set()) else 0


class TestDryRunMain(unittest.TestCase):
    def test_flow(self):
        import pygame
        js_holder = {}

        def make_js(idx):
            js_holder["js"] = FakeJoystick(idx)
            return js_holder["js"]

        def btn(b, down=True):
            t = pygame.JOYBUTTONDOWN if down else pygame.JOYBUTTONUP
            pygame.event.post(pygame.event.Event(t, instance_id=7, button=b, joy=0))

        # フレームごとの台本 (ドライランは 1 回 0.3 秒動いている扱い = 60fps で 18 コマ程度)
        script = {
            1: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            3: lambda: (btn(gt.BTN_RIGHT), btn(gt.BTN_RIGHT, False)),
            5: lambda: (btn(gt.BTN_RIGHT), btn(gt.BTN_RIGHT, False)),       # 動いている間 → 終わってから送る (11/5)
            40: lambda: btn(gt.BTN_TRIANGLE),
            42: lambda: (btn(gt.BTN_DOWN), btn(gt.BTN_DOWN, False)),
            80: lambda: btn(gt.BTN_CROSS),
            82: lambda: js_holder["js"].axes.__setitem__(slice(4, 6), [0.5, 0.5]),   # 停止
            85: lambda: (btn(gt.BTN_LEFT), btn(gt.BTN_LEFT, False)),
            86: lambda: js_holder["js"].axes.__setitem__(slice(4, 6), [-1.0, -1.0]),
            88: lambda: btn(gt.BTN_OPTIONS),
            150: lambda: btn(gt.BTN_HOME),
            230: lambda: btn(gt.BTN_HOME, False),
            300: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }

        def on_frame(n, tp):
            if n in script:
                script[n]()

        logs = []

        class SpyNullLog(gt.NullLog):         # 書かないことは変えずに, 渡された行だけ見る
            def __init__(self):
                self.rows = []
                logs.append(self)

            def write(self, rec):
                json.dumps(rec, ensure_ascii=False)
                self.rows.append(rec)

        out = []
        with tempfile.TemporaryDirectory() as td, \
                mock.patch.object(gt, "LOG_ROOT", Path(td)), \
                mock.patch.object(gt, "ensure_ssd_mounted", side_effect=AssertionError("KIOXIA を確かめた")), \
                mock.patch.object(gt, "load_daemon_modules", side_effect=AssertionError("daemon の送り口を読んだ")), \
                mock.patch.object(gt, "JsonlLog", side_effect=AssertionError("log.jsonl を開いた")), \
                mock.patch.object(gt, "write_json_atomic", side_effect=AssertionError("meta.json を書いた")), \
                mock.patch.object(gt.shutil, "copy2", side_effect=AssertionError("スクリプトを写した")), \
                mock.patch.object(gt, "NullLog", SpyNullLog), \
                mock.patch.object(gt.pygame.joystick, "Joystick", make_js), \
                mock.patch.object(gt, "GRIPPER_SLEEP", 0.2), \
                mock.patch.object(gt, "get_daemon_socket", side_effect=AssertionError("daemon に触った")), \
                mock.patch.object(gt, "daemon_send_angles", side_effect=AssertionError("daemon に送った")), \
                mock.patch("builtins.print", lambda *a, **k: out.append(" ".join(str(x) for x in a))):
            gt.main([], on_frame=on_frame)
            self.assertEqual(list(Path(td).iterdir()), [])        # フォルダも作らない
        self.assertEqual(len(logs), 1)
        rows = logs[0].rows
        self.assertIn("[記録] ドライランなので記録しません", out)
        self.assertIn("[終了] アームは動かしていません。記録なし（ドライラン）", out)
        kinds = [(r["kind"], r.get("accepted"), r.get("reason")) for r in rows]
        # 受け付けた 1 回目の行は届いてから書くので, 動いている間に断った行 (busy) が先に来る
        self.assertIn(("move", True), [k[:2] for k in kinds])
        self.assertIn(("select_joint", True, None), kinds)
        self.assertIn(("stop", True, "L2+R2"), kinds)
        self.assertIn(("move", False, "stopped"), kinds)
        self.assertIn(("resume", True, None), kinds)
        home = [r for r in rows if r["kind"] == "home"]
        self.assertEqual(len(home), 1)
        self.assertEqual(home[0]["command"], HOME)
        moves = [r for r in rows if r["kind"] == "move" and r.get("sent") is True]
        self.assertEqual(moves[0]["command"], [0.5, 0, 0, 0, 0, -30])
        self.assertEqual(moves[1]["command"], [1.0, 0, 0, 0, 0, -30])        # 動いている間の押しは 終わってから (11/5)
        self.assertEqual(moves[2]["command"], [1.0, 0.0, 0.5, 0, 0, -30])    # △ で j2 にしてから下
        for r in moves:
            for key in ("t", "button", "kind", "selected_joint", "step_deg", "measured_before",
                        "command", "reached_angles", "reached", "grip_state", "clip", "stopped"):
                self.assertIn(key, r)
        self.assertTrue(all(r["execute"] is False for r in rows))


class TestEmbeddedFinish(unittest.TestCase):
    """run_policy_client の p から呼ぶ run_embedded (2026-11-05 LOG450): □ で戻る. 動いている間の □ は断る."""

    def test_square_finishes_only_when_idle(self):
        import pygame

        def btn(b, down=True):
            t = pygame.JOYBUTTONDOWN if down else pygame.JOYBUTTONUP
            pygame.event.post(pygame.event.Event(t, instance_id=7, button=b, joy=0))

        seen = {}

        def on_frame(n, tp):
            if n == 1:
                pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0))
            elif n == 3:
                btn(gt.BTN_RIGHT); btn(gt.BTN_RIGHT, False)
            elif n == 5:
                seen["busy_at_square"] = tp.busy
                btn(gt.BTN_SQUARE); btn(gt.BTN_SQUARE, False)
            elif n == 80:
                btn(gt.BTN_SQUARE); btn(gt.BTN_SQUARE, False)
            elif n == 400:
                pygame.event.post(pygame.event.Event(pygame.QUIT))

        rows, out = [], []

        class L:
            def write(self, r):
                json.dumps(r, ensure_ascii=False)
                rows.append(r)

            def close(self):
                pass

        arm = gt.DryArm(HOME)
        with mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
                mock.patch.object(gt, "GRIPPER_SLEEP", 0.0):
            reason, tp, done = gt.run_embedded(arm, np.array(HOME, dtype=float), L(), grip=1,
                                               out=out.append, on_frame=on_frame)
        self.assertEqual(reason, "finish_button")
        self.assertTrue(done)
        self.assertTrue(seen["busy_at_square"])
        self.assertTrue(any("動いている間は終われません" in x for x in out))
        self.assertEqual(tp.grip, "closed")
        sent = [r for r in rows if r["kind"] == "move" and r.get("sent") is True]
        self.assertEqual(len(sent), 1)
        self.assertNotIn("square", [r.get("button_name") for r in rows])     # □ は Teleop に渡さない
        self.assertAlmostEqual(arm.q[0], HOME[0] + gt.STEP_FINE_DEG)


class TestExecuteMainRecords(unittest.TestCase):
    """--execute は今までどおり KIOXIA (ここでは一時フォルダ) に log.jsonl・meta.json・写しを書く."""

    def test_execute_writes(self):
        import pygame
        script = {
            1: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            3: lambda: (pygame.event.post(pygame.event.Event(pygame.JOYBUTTONDOWN, instance_id=7,
                                                             button=gt.BTN_RIGHT, joy=0)),
                        pygame.event.post(pygame.event.Event(pygame.JOYBUTTONUP, instance_id=7,
                                                             button=gt.BTN_RIGHT, joy=0))),
            60: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }
        sends = []
        q = {"now": list(HOME)}

        def send(a, **k):
            sends.append(list(a))
            q["now"] = list(a)

        with tempfile.TemporaryDirectory() as td, \
                mock.patch.object(gt, "LOG_ROOT", Path(td)), \
                mock.patch.object(gt, "ensure_ssd_mounted", lambda: None), \
                mock.patch.object(gt.pygame.joystick, "Joystick", FakeJoystick), \
                mock.patch.object(gt, "get_daemon_socket", lambda: None), \
                mock.patch.object(gt, "close_daemon_socket", lambda: None), \
                mock.patch.object(gt.rpc, "install_daemon_lock", lambda: None), \
                mock.patch.object(gt.rpc, "read_angles", lambda: np.array(q["now"], dtype=float)), \
                mock.patch.object(gt, "daemon_send_angles", send), \
                mock.patch.object(gt, "daemon_request", lambda c, **k: (sends.append(("send_angle", c["joint_id"], c["angle"]))
                                                                        or {"ok": True})), \
                mock.patch("builtins.print", lambda *a, **k: None):
            gt.main(["--execute"], on_frame=lambda n, tp: script[n]() if n in script else None)
            runs = list(Path(td).iterdir())
            self.assertEqual(len(runs), 1)
            run = runs[0]
            self.assertFalse(run.name.endswith("_dry"))
            rows = [json.loads(x) for x in (run / "log.jsonl").read_text().splitlines()]
            meta = json.loads((run / "meta.json").read_text())
            self.assertTrue((run / "gamepad_teleop.py").exists())
        self.assertEqual(sends, [("send_angle", 1, 0.5)])   # 十字キーは動かす関節だけを送る (11/5)
        self.assertEqual([(r["kind"], r.get("sent")) for r in rows], [("move", True)])
        self.assertTrue(rows[0]["execute"])
        self.assertEqual(meta["controllers"][0]["name"], "PS4 Controller")
        self.assertEqual(meta["controllers"][0]["guid"], "fake-guid")
        self.assertTrue(meta["execute"])
        self.assertEqual(meta["end_reason"], "window_closed")
        self.assertEqual(meta["n_sent"], 1)
        self.assertEqual(meta["guards"]["J1_MAX_DEG"], 90.87)
        self.assertEqual(meta["motion"]["speed"], 20)
        for key in ("python", "pygame", "sdl", "start_pose", "git"):
            self.assertIn(key, meta)


class TestNoCv2InDryRun(unittest.TestCase):
    """依頼 69: ドライランは cv2 (cv2/.dylibs/libSDL2) を読まず, objc の重複の警告が出ない.
    このプロセスはもう load_daemon_modules() を呼んでいるので, 別のプロセスで main([]) を回す."""

    CHILD = r'''
import os, sys, tempfile
os.environ["SDL_VIDEODRIVER"] = "dummy"; os.environ["SDL_AUDIODRIVER"] = "dummy"
sys.path.insert(0, sys.argv[1])
from pathlib import Path
import gamepad_teleop as gt
import pygame
td = tempfile.mkdtemp()
gt.LOG_ROOT = Path(td)
gt.SSD_MOUNT = "/nonexistent_kioxia_for_test"     # KIOXIA がなくても動く
gt.main([], on_frame=lambda n, tp: pygame.event.post(pygame.event.Event(pygame.QUIT)) if n == 5 else None)
assert os.listdir(td) == [], os.listdir(td)
bad = [m for m in ("cv2", "run_policy_client", "visual_servo_pick", "j0_sweep_scan") if m in sys.modules]
assert not bad, bad
assert gt.rpc is None and gt.daemon_send_angles is None
print("CHILD_OK")
'''

    def test_dry_run_in_fresh_process(self):
        r = subprocess.run([sys.executable, "-c", self.CHILD, str(Path(gt.__file__).parent)],
                           capture_output=True, text=True, timeout=60)
        self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
        self.assertIn("CHILD_OK", r.stdout)
        self.assertNotIn("is implemented in both", r.stderr)


class TestConstantsMatchModules(unittest.TestCase):
    """読み込まずに読んだ値が, 読み込んだモジュールの値と同じ."""

    def test_same(self):
        self.assertEqual(gt.HOME, [float(v) for v in gt.rpc.HOME])
        self.assertEqual(gt.J4_SAFE, tuple(float(v) for v in gt.rpc.J4_SAFE))
        self.assertEqual(gt.GRIPPER_SLEEP, gt.rpc.GRIPPER_SLEEP)
        self.assertEqual(gt.JOINT_LIMITS, [tuple(float(v) for v in x) for x in gt.wr.JOINT_LIMITS])
        self.assertEqual((gt.SPEED, gt.POLL_SLEEP, gt.REACH_TIMEOUT_SEC), (20, 0.3, 8.0))

    def test_read_constants_rejects_double_assignment(self):
        with tempfile.TemporaryDirectory() as td:
            f = Path(td) / "m.py"
            f.write_text("A = 1\nA = 2\n")
            with self.assertRaises(RuntimeError):
                gt.read_constants(f, ("A",))
            with self.assertRaises(RuntimeError):
                gt.read_constants(f, ("B",))
            for src in ("A = 1\nA += 1\n", "A = 1\nA: int = 2\n", "A = 1\nB = A = 2\n",
                        "A = 1\nfor A in []:\n    pass\n", "A = 1\ndef f():\n    global A\n    A = 2\n"):
                f.write_text(src)
                with self.assertRaises(RuntimeError, msg=src):
                    gt.read_constants(f, ("A",))
            f.write_text("A = 1\ndef f():\n    return A\n")
            self.assertEqual(gt.read_constants(f, ("A",)), {"A": 1})


class TestExecuteLoadGuards(unittest.TestCase):
    def test_no_load_when_ssd_missing(self):
        with mock.patch.object(gt, "ensure_ssd_mounted", side_effect=SystemExit("no ssd")), \
                mock.patch.object(gt, "load_daemon_modules") as load:
            with self.assertRaises(SystemExit) as cm:
                gt.main(["--execute"])
        self.assertEqual(str(cm.exception), "no ssd")
        load.assert_not_called()

    def test_load_failure_aborts_before_record(self):
        with tempfile.TemporaryDirectory() as td, \
                mock.patch.object(gt, "LOG_ROOT", Path(td)), \
                mock.patch.object(gt, "ensure_ssd_mounted", lambda: None), \
                mock.patch.object(gt, "load_daemon_modules", side_effect=ImportError("x")), \
                mock.patch.object(gt, "get_daemon_socket", side_effect=AssertionError("daemon に触った")):
            with self.assertRaises(SystemExit) as cm:
                gt.main(["--execute"])
            self.assertIn("[ABORT]", str(cm.exception))
            self.assertEqual(list(Path(td).iterdir()), [])

    def test_mismatch_aborts_in_fresh_process(self):
        """ファイルから読んだ値と読み込んだ値が違えば, daemon の送り口を入れずに止める (別のプロセス)."""
        child = r'''
import os, sys, tempfile
os.environ["SDL_VIDEODRIVER"] = "dummy"
sys.path.insert(0, sys.argv[1])
from pathlib import Path
import gamepad_teleop as gt
gt.HOME = [1.0, 0.0, 0.0, 0.0, 0.0, -30.0]
td = tempfile.mkdtemp()
gt.LOG_ROOT = Path(td)
gt.ensure_ssd_mounted = lambda: None
try:
    gt.main(["--execute"])
except SystemExit as e:
    assert "HOME" in str(e), e
else:
    raise AssertionError("止まらなかった")
assert os.listdir(td) == [], os.listdir(td)
assert gt.rpc is None and gt.daemon_send_angles is None and gt.get_daemon_socket is None
print("CHILD_OK")
'''
        r = subprocess.run([sys.executable, "-c", child, str(Path(gt.__file__).parent)],
                           capture_output=True, text=True, timeout=60)
        self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
        self.assertIn("CHILD_OK", r.stdout)


if __name__ == "__main__":
    unittest.main()
```

### pi05/diag_joint_hold.py（手首の点検・commit 284dfd7）

#### pi05/diag_joint_hold.py

```python
#!/usr/bin/env python3
"""diag_joint_hold.py — 実機推論の記録から, 関節ごとの「動いている途中の遅れ」と「止まったあとのずれ (垂れ)」を出す (2026-11-01 依頼 57).

読むだけ (runs/ には書かない). アームは使わない.

入力: runs/pi05_policy/{run_stem}/recording/states.csv と log.jsonl
  states.csv (依頼 46 のあとの記録) には send_angles 行 (送った時刻・目標・rpc_sec) と, 爪の rpc 行がある.
  依頼 46 より前の記録 (send_angles 行がない) は log.jsonl だけで見る (下の「log で見る」).

分け方 (states.csv で見る):
  - 指令 = 直前の send_angles の目標. 送ってから rpc が返るまで (daemon が着いたと判定するまで. 約 1.6 秒) は
    つながりがふさがり, 実測は残らない. rpc が返ったあとから次の送信までを「止まっているはずの区間」(hold) とする.
    爪の rpc (開け閉め. 指令は変わらない) でも区間を切る (爪の前・爪の後 を別に見る).
  - 区間の中のコマごと・関節ごとに, 前後 MOVE_WIN_SEC 以内の実測のふれが MOVE_EPS_DEG を超えていれば「動いている」.
    動いているコマの |実測 − 指令| を「動いている途中の遅れ」とする (着いたと判定されたあとも動いていた分も含む).
  - 動いていないコマの |実測 − 指令| を「止まったあとのずれ」とする. 関節ごとに, 区間の最後の動いていないコマの値を
    その区間のずれとし (区間の数で数える. 長く待った区間が重くならない), run ごとに中央値・90%・最大を出す.
    codex R1 必須: 動いているかは関節ごとに見る (j1 が動き続けていても, 止まった j4 のずれを見落とさない).
    その関節が区間の中で一度も止まっていなければ, その区間のずれは無し (NaN) とする.
  - 区間の最後の「止まりが続いたところ」(関節ごと. つながった動いていないコマ) の変化 (最後 − 最初) を
    「止まっている間の変化」(じわじわ垂れる) とし, 続いた秒も出す. MOVE_EPS_DEG に満たない速さで動き続けるものはここに出る.
  - 前後 0.3 秒の窓で動きが見えなかったコマを「動いていない」としている (区間の端・まばらな記録では 1 コマでも
    動いていないことになる). 止まりを確かめた値ではなく, この窓で動きを見つけられなかった値として読む (codex R1)
  - 粗い値 (依頼文と同じ): 送信のあとのすべての実測と直前の指令の差の最大.

ゲームパッドの補正 (2026-11-06 依頼 97. run_policy_client の p. コミット cd2fb46):
  - 送り方が違う: 1 刻みは動かす関節だけを send_angle で送り, 長押しは 15° 先へ 1 回送って離したら stop で止める.
    states.csv には rpc 行 (cmd=send_angle / cmd=stop) しか残らず, 目標は log.jsonl の type=pad の行にある.
    そこで rpc 行を pad の行と時刻で組にし, 指令をゲームパッドと同じ決まりで作る:
      1 刻み (pad_step)  : 指令 = その行の command (6 関節. 動かさない関節は補正を始めたときの指令のまま)
      長押し (pad_hold)  : 指令 = command (15° 先). 押した関節はそこへ着く気がないので, この区間ではその関節を
                           見ない (遅れ 0・止まりのずれ なし). ほかの関節は見る
      止める (pad_stop)  : 指令 = 直前の指令のうち, 止めた関節だけ止めたあとに読んだ角度 (measured_after_halt)
                           (gamepad_teleop の _apply_halt と同じ). 止めた関節が分からなければ, 読んだ角度そのもの
    組にできなかった rpc 行は pad_unknown として指令を持たない区間にする (値を出さない. 数だけ出す)
  - daemon が送ったらすぐ返す (2026-11-05 c7992ca のあと. rpc 0.03〜0.14 秒) と, rpc が返ったあとも腕は指令へ
    向かって動いている. その動きは「着いたはずなのに動いた」ではない. rpc が FAST_RPC_SEC より短く返った区間では:
      - 動いている途中の遅れは数えない (指令へ向かう途中. 途中で一度止まってまた向かうものも)
      - 代わりに関節ごとに「それまでにいちばん指令へ近づいた所から離れた分」(|実測−指令| − それまでの最小) を
        away として出し, EVENT_DEG を超えたら外れに数える (着いたあとに垂れた・向かう途中に逆へ動いた).
        すぐ返った区間がつづき, その関節の指令が変わらない間は, 区間をまたいで最小を引き継ぐ (codex R1 必須)
      - 止まったあとのずれは, その関節が区間の中で動いたなら動き出したあとの止まりだけで見る
        (動き出す前の止まりは前の姿勢のままなので, 新しい指令とのずれにしない)
    1.6 秒待って返っていた記録 (11-01 まで) はこれまでと同じ (向かう途中は rpc の間で, 実測がない)

log で見る (依頼 46 より前の記録とも比べられる):
  - 補正の 1 刻み: measured_after − after (全 6 関節)
  - チャンクの区切り: 次の推論で読んだ measured_state − 直前の指令 (チャンクの最後の sent_targets / 補正の after)

Usage:
  python3 pi05/diag_joint_hold.py --run pi05_20261101_190347 --run pi05_20261101_194042
  python3 pi05/diag_joint_hold.py --run ... --json docs/diag_joint_hold_20261101.json
"""
import argparse
import csv
import json
import sys
from pathlib import Path

import numpy as np

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
RUNS_ROOT = DATA_ROOT / "runs" / "pi05_policy"
JOINTS = ["j0", "j1", "j2", "j3", "j4", "j5"]
MOVE_WIN_SEC = 0.3      # 前後これだけの間の実測のふれで「動いている」を見る
MOVE_EPS_DEG = 0.3      # ふれがこれを超えたら動いている (読みの刻みは約 0.09°)
LONG_HOLD_SEC = 1.0     # 「止まっている間の変化」は, 動いていないコマがこれだけ続いた区間で見る
# 指令が変わらないのに動いた・止まったあとも残ったずれがこれを超えた区間を「外れ」として数える (依頼 57).
# 手首の外れのなかった回 (190347・190854・192101) の止まったあとの j4 のずれは最大 3.5° (中央値 0.3〜0.6°).
# Log17 (10-31) のチャンクの区切りでも最大 2.8° (182545 の 8.1° を除く)
EVENT_DEG = 4.0
# rpc がこれより短く返った区間は「送ったらすぐ返った」(着くのを待っていない) とみなす (依頼 97).
# 11-01 までは 1.5〜1.6 秒, 11-06 は 0.01〜0.14 秒 (send_angles・send_angle・stop)
FAST_RPC_SEC = 0.5
PAD_MATCH_SEC = 0.2     # rpc 行と pad の行 (t_send / t_halt) の時刻の差の上限


def load_rows(path):
    rows = []
    with Path(path).open(newline="") as f:
        for r in csv.DictReader(f):
            try:
                r["_ts"] = float(r["ts"])
            except Exception:
                continue
            rows.append(r)
    rows.sort(key=lambda r: r["_ts"])
    return rows


def _q(r):
    try:
        return np.array([float(r[j]) for j in JOINTS])
    except Exception:
        return None


def _rpc_sec(r):
    for tok in (r.get("note") or "").split():
        if tok.startswith("rpc_sec="):
            try:
                return float(tok.split("=", 1)[1])
            except ValueError:
                return 0.0
    return 0.0


def pad_events(log):
    """log.jsonl の type=pad の行から, 送ったもの (move / home) と止めたもの (halt) を時刻の順に集める (依頼 97)."""
    sends, halts = [], []
    for r in log or ():
        if r.get("type") != "pad":
            continue
        if r.get("kind") in ("move", "home") and r.get("sent") is True and r.get("command") is not None \
                and r.get("t_send") is not None:
            sends.append({"t": float(r["t_send"]), "cmd": np.asarray(r["command"][:6], float),
                          "joint": r.get("joint") if r.get("kind") == "move" else None,
                          "hold": bool(r.get("no_check")), "resends": len(r.get("resends") or []),
                          "corr": r.get("correction_id")})
        elif r.get("kind") == "halt" and r.get("t_halt") is not None:
            q = r.get("measured_after_halt")
            halts.append({"t": float(r["t_halt"]), "q": None if q is None else np.asarray(q[:6], float),
                          "joint": r.get("halt_joint"), "how": r.get("halt"), "corr": r.get("correction_id")})
    sends.sort(key=lambda x: x["t"]); halts.sort(key=lambda x: x["t"])
    return sends, halts


def _pad_send_for(sends, used, ts):
    """send_angle の rpc 行 (時刻 ts) に当たる pad の送信と, それが何回目か (0 = 初めて・1 以上 = 送り直し).
    codex R1 必須: 初めての送信は, まだ組にしていない送信のうち |t_send − ts| が PAD_MATCH_SEC 以内でいちばん近いもの.
    それがなければ, ts より前に組にした送信のうち送り直しの残りがあるいちばん新しいものの送り直しとする.
    どちらもなければ (None, 0). used = {送信の番号: 組にした回数}"""
    cand = [(abs(x["t"] - ts), k) for k, x in enumerate(sends) if k not in used and abs(x["t"] - ts) <= PAD_MATCH_SEC]
    if cand:
        k = min(cand)[1]
        used[k] = 1
        return sends[k], 0
    for k in sorted((k for k in used if sends[k]["t"] <= ts), key=lambda k: sends[k]["t"], reverse=True):
        if used[k] <= sends[k]["resends"]:
            used[k] += 1
            return sends[k], used[k] - 1
        break               # いちばん新しい送信の送り直しが尽きていれば, それより前のものにはしない
    return None, 0


def segments(rows, log=None):
    """hold の区間に分ける. 戻り値: [{kind, stage, cmd, t_cmd, t0 (rpc が返った時刻), fast, skip, ts[], q[], stages[]}]
    log (log.jsonl の行) があれば, ゲームパッドの rpc 行 (send_angle / stop) の指令を pad の行から作る (依頼 97)."""
    sends, halts = pad_events(log)
    used_sends, used_halts = {}, set()
    segs, cur = [], None
    nan6 = np.full(6, np.nan)
    for r in rows:
        ev = r.get("event_type")
        if ev == "send_angles":
            q = _q(r)
            if q is None:
                continue
            rs = _rpc_sec(r)
            cur = {"kind": "send", "stage": r.get("stage"), "cmd": q, "t_cmd": r["_ts"],
                   "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC, "skip": None,
                   "ts": [], "q": [], "stages": []}
            segs.append(cur)
        elif ev == "rpc" and cur is not None:
            cmd = (r.get("note") or "").split()[0].replace("cmd=", "")
            rs = _rpc_sec(r)
            base = {"stage": r.get("stage"), "t_cmd": r["_ts"], "t0": r["_ts"] + rs, "fast": rs < FAST_RPC_SEC,
                    "skip": None, "ts": [], "q": [], "stages": []}
            if cmd == "send_angle":
                x, n_used = _pad_send_for(sends, used_sends, r["_ts"])
                if x is None:
                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
                else:
                    cur = dict(base, kind="pad_hold" if x["hold"] else "pad_step", cmd=x["cmd"].copy(),
                               skip=x["joint"] if x["hold"] else None, t_cmd=x["t"] if n_used == 0 else r["_ts"])
            elif cmd == "stop":
                # codex R1 推奨: いちばん近い・まだ組にしていない halt (一対一)
                hc = [(abs(x["t"] - r["_ts"]), k) for k, x in enumerate(halts)
                      if k not in used_halts and abs(x["t"] - r["_ts"]) <= PAD_MATCH_SEC]
                h = None
                if hc:
                    used_halts.add(min(hc)[1])
                    h = halts[min(hc)[1]]
                if h is None or h["q"] is None:
                    cur = dict(base, kind="pad_unknown", cmd=nan6.copy())
                else:
                    c = np.asarray(cur["cmd"], float).copy()
                    if h["joint"] is not None and not np.isnan(c).any():
                        c[int(h["joint"])] = h["q"][int(h["joint"])]
                    else:
                        c = h["q"].copy()
                    cur = dict(base, kind="pad_stop", cmd=c)
            else:
                cur = dict(base, kind="grip_after", grip_cmd=cmd, cmd=cur["cmd"], t_cmd=cur["t_cmd"],
                           fast=cur.get("fast", False))
            segs.append(cur)
        elif ev == "actual" and cur is not None:
            q = _q(r)
            if q is None or r["_ts"] < cur["t0"] - 0.005:
                continue
            cur["ts"].append(r["_ts"]); cur["q"].append(q); cur["stages"].append(r.get("stage"))
    for s in segs:
        s["ts"] = np.array(s["ts"]); s["q"] = np.array(s["q"]).reshape(-1, 6)
    return segs


def moving_mask(ts, q, win=MOVE_WIN_SEC, eps=MOVE_EPS_DEG):
    """(n, 6) 関節ごとに, 前後 win 秒以内の実測のふれが eps を超えるか."""
    n = len(ts)
    mv = np.zeros((n, 6), dtype=bool)
    for i in range(n):
        m = np.abs(ts - ts[i]) <= win
        mv[i] = (q[m].max(axis=0) - q[m].min(axis=0)) > eps
    return mv


def analyze_segments(segs):
    """区間ごとの値 (関節ごと. 止まっていない関節は NaN).
    戻り値: [{kind, stage, t, cmd, hold_err (6), lag_max (6), drift (6), still_sec (6)}]"""
    out = []
    nan6 = np.full(6, np.nan)
    # codex R1 必須: 送ったらすぐ返った区間がつづき, その関節の指令が変わらない間は, それまでの最小のずれを引き継ぐ
    # (ほかの関節の送信・送り直し・爪で区間が切れても, 0→3° と 3→6° に分かれた垂れを 6° として見る)
    carry_cmd, carry_min = nan6.copy(), nan6.copy()
    for k, s in enumerate(segs):
        n = len(s["ts"])
        rec = {"seg": k, "kind": s["kind"], "stage": s["stages"][-1] if n else s["stage"], "t": s["t_cmd"],
               "t0": s["t0"], "cmd": s["cmd"], "n": n, "hold_err": nan6.copy(), "lag_max": np.zeros(6),
               "drift": nan6.copy(), "still_sec": np.zeros(6), "grip_cmd": s.get("grip_cmd")}
        rec["away_max"] = np.zeros(6)
        rec["skip"] = s.get("skip")
        rec["fast"] = bool(s.get("fast"))
        # codex R2 必須: 引き継ぎを切るかは, 実測があるかに関係なく区間ごとに決める.
        # すぐ返らない区間・指令の分からない区間は全関節, 指令が変わった関節・長押しの関節はその関節を切る
        if not s.get("fast") or np.isnan(s["cmd"]).any():
            carry_cmd, carry_min = nan6.copy(), nan6.copy()
        else:
            changed = ~np.isclose(s["cmd"], carry_cmd, atol=1e-6, rtol=0)
            carry_min = np.where(changed, np.nan, carry_min)
            carry_cmd = s["cmd"].copy()
            if s.get("skip") is not None:
                carry_min[int(s["skip"])] = np.nan
        if n and not np.isnan(s["cmd"]).any():
            err = s["q"] - s["cmd"]
            mv = moving_mask(s["ts"], s["q"])
            if s.get("fast"):
                # 依頼 97: 送ったらすぐ返った区間. 指令へ向かう動き (途中で一度止まって また向かうものも) は遅れに
                # 数えない. 代わりに「それまでにいちばん指令へ近づいた所から離れた分」を away に出す
                # (着いたあとに垂れた・向かう途中に逆へ動いた. 関節ごと)
                a = np.abs(err)
                start = np.fmin(carry_min, a[0])                 # 切った関節 (NaN) は区間の頭から
                run_min = np.minimum.accumulate(np.vstack([start, a]), axis=0)[1:]
                rec["away_max"] = np.max(a - run_min, axis=0)
                carry_min = run_min[-1].copy()
                if s.get("skip") is not None:
                    carry_min[int(s["skip"])] = np.nan
                lag_mv = np.zeros_like(mv)
            else:
                lag_mv = mv
            rec["lag_max"] = np.where(lag_mv.any(axis=0), np.where(lag_mv, np.abs(err), 0).max(axis=0), 0.0)
            for i in range(6):
                if s.get("skip") is not None and i == int(s["skip"]):
                    continue        # 長押しの関節は 15° 先へ着く気がない (依頼 97)
                idx = np.flatnonzero(~mv[:, i])
                if s.get("fast") and mv[:, i].any():
                    # 依頼 97: すぐ返った区間では, 動き出す前の止まり (前の姿勢のまま) をずれに使わない
                    idx = idx[idx > np.flatnonzero(mv[:, i])[0]]
                if not len(idx):
                    continue
                last = idx[-1]
                first = last
                while first - 1 >= 0 and not mv[first - 1, i]:   # 最後の止まりが続いたところの頭
                    first -= 1
                rec["hold_err"][i] = err[last, i]
                rec["drift"][i] = s["q"][last, i] - s["q"][first, i]
                rec["still_sec"][i] = float(s["ts"][last] - s["ts"][first])
            if s.get("skip") is not None:
                rec["lag_max"][int(s["skip"])] = 0.0
                rec["away_max"][int(s["skip"])] = 0.0
            ce = np.abs(err)
            if s.get("skip") is not None:
                ce[:, int(s["skip"])] = 0.0
            rec["coarse_max"] = ce.max(axis=0)
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        elif n:
            rec["first_q"] = s["q"][0]; rec["last_q"] = s["q"][-1]
        out.append(rec)
    return out


def _has(v):
    return v is not None and not np.isnan(v)


def log_hold_errors(log):
    """log.jsonl だけで見る止まったあとのずれ. 戻り値: {"correction": [(t, err6)], "chunk": [(t, err6)]}"""
    out = {"correction": [], "chunk": []}
    last_cmd = None
    for r in log:
        if r.get("type") is None and "chunk" in r:
            ms = r.get("measured_state")
            if last_cmd is not None and ms is not None and r.get("state_source") is not None:
                out["chunk"].append((float(r["t"]), np.asarray(ms[:6], float) - last_cmd))
            if r.get("executed") and r.get("sent_targets"):
                last_cmd = np.asarray(r["sent_targets"][-1][:6], float)
        elif r.get("type") == "correction" and r.get("action") == "move" and r.get("sent"):
            if r.get("measured_after") is not None:
                out["correction"].append((float(r["t"]), np.asarray(r["measured_after"][:6], float)
                                          - np.asarray(r["after"][:6], float)))
            last_cmd = np.asarray(r["after"][:6], float)
        elif r.get("type") == "rewind":
            last_cmd = None
    return out


def _stats(vals):
    a = np.abs(np.asarray(vals, float))
    if not len(a):
        return None
    return {"n": int(len(a)), "median": round(float(np.median(a)), 2),
            "p90": round(float(np.percentile(a, 90)), 2), "max": round(float(a.max()), 2)}


def summarize_run(run_dir):
    run_dir = Path(run_dir)
    stem = run_dir.name
    trial = {}
    try:
        trial = json.loads((run_dir / f"{stem}.json").read_text())
    except Exception:
        pass
    log = []
    lp = run_dir / "log.jsonl"
    if lp.exists():
        for line in lp.read_text().splitlines():
            try:
                log.append(json.loads(line))
            except Exception:
                pass
    res = {"run": stem, "outcome": trial.get("outcome"), "corrections": trial.get("corrections")}
    rows = load_rows(run_dir / "recording" / "states.csv")
    segs = analyze_segments(segments(rows, log))
    res["has_send_rows"] = bool(segs)
    lh = log_hold_errors(log)
    res["log"] = {k: {j: _stats([e[i] for _, e in v]) for i, j in enumerate(JOINTS)} for k, v in lh.items()}
    if not segs:
        return res, segs
    work = [s for s in segs if s["stage"] != "home"]   # HOME は学習に入らない. 別に出す
    res["segments"] = len(segs)
    res["work_segments"] = len(work)
    res["coarse_max"] = {j: round(float(max(s.get("coarse_max", np.zeros(6))[i] for s in segs)), 2)
                         for i, j in enumerate(JOINTS)}
    # codex R1 必須: HOME の区間しかない記録でも止まらない (学習に入る区間がなければ, その値は 0 / None)
    res["lag_max"] = {j: round(float(max([s["lag_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}

    def holds(i, ss=work):
        return [float(s["hold_err"][i]) for s in ss if _has(s["hold_err"][i])]
    res["hold"] = {j: _stats(holds(i)) for i, j in enumerate(JOINTS)}
    res["hold_signed_median"] = ({j: round(float(np.median(holds(i))), 2) if holds(i) else None
                                  for i, j in enumerate(JOINTS)} if work else None)
    by = {}
    for s in work:
        key = "grip_after" if s["kind"] == "grip_after" else s["stage"]
        by.setdefault(key, []).append(s)

    def _mx(v):
        return round(float(max(np.abs(v))), 2) if v else None
    res["hold_by_context"] = {k: {"n": len(v), "j4_max": _mx(holds(4, v)),
                                  "j4_median": round(float(np.median(np.abs(holds(4, v)))), 2) if holds(4, v) else None,
                                  "j1_max": _mx(holds(1, v)), "j2_max": _mx(holds(2, v))}
                              for k, v in sorted(by.items())}
    # 学習に入る段 (HOME 以外) で, 止まったあとのずれ・動いている途中の遅れのどちらかが EVENT_DEG を超えた区間・関節の組
    ev = []
    for s in work:
        for i, j in enumerate(JOINTS):
            he = float(s["hold_err"][i]) if _has(s["hold_err"][i]) else None
            if (he is not None and abs(he) > EVENT_DEG) or s["lag_max"][i] > EVENT_DEG or s["away_max"][i] > EVENT_DEG:
                e = {"seg": s["seg"], "sec": round(s["t"] - segs[0]["t"], 1), "stage": s["stage"], "kind": s["kind"],
                     "joint": j, "cmd": round(float(s["cmd"][i]), 2),
                     "first": round(float(s["first_q"][i]), 2), "last": round(float(s["last_q"][i]), 2),
                     "hold_err": None if he is None else round(he, 2),
                     "lag_max": round(float(s["lag_max"][i]), 2)}
                if s["away_max"][i] > 0:
                    e["away_max"] = round(float(s["away_max"][i]), 2)      # 依頼 97 (送ったらすぐ返った区間だけ)
                ev.append(e)
    res["events"] = ev
    # 依頼 97: 送ったらすぐ返った区間の数・ゲームパッドの区間の数・組にできなかった rpc 行の数
    res["fast_segments"] = sum(1 for s in segs if s.get("fast"))
    res["pad_segments"] = {k: sum(1 for s in segs if s["kind"] == k) for k in ("pad_step", "pad_hold", "pad_stop",
                                                                             "pad_unknown")}
    res["away_max"] = {j: round(float(max([s["away_max"][i] for s in work] or [0.0])), 2) for i, j in enumerate(JOINTS)}
    res["event_segments"] = len({e["seg"] for e in ev})   # codex R1・R2 推奨: 区間の数 (区間の番号で数える. 関節は重ねない)
    res["drift_max"] = {j: round(float(max([abs(s["drift"][i]) for s in work
                                            if _has(s["drift"][i]) and s["still_sec"][i] >= LONG_HOLD_SEC] or [0.0])), 2)
                        for i, j in enumerate(JOINTS)}
    # 爪の開け閉めの前後: 爪の rpc の直前の区間の最後の実測 → 爪のあとの区間の最初の実測
    grips = []
    for a, b in zip(segs, segs[1:]):
        if b["kind"] == "grip_after" and a["n"] and b["n"]:
            grips.append({"t": round(b["t0"], 2), "cmd": b["grip_cmd"], "stage": b["stage"],
                          "delta": [round(float(v), 2) for v in b["first_q"] - a["last_q"]],
                          # codex R2 必須: 爪のあとに止まらなかった関節は None (最初の実測のずれで埋めない. それは別に出す)
                          "err_after": [round(float(h), 2) if _has(h) else None for h in b["hold_err"]],
                          "err_first": [round(float(v), 2) for v in b["first_q"] - b["cmd"]]})
    res["grip"] = grips
    homes = [s for s in segs if s["stage"] == "home" and s["n"]]
    res["home"] = [{"t": round(s["t"], 2), "err_last": [round(float(v), 2) for v in s["last_q"] - s["cmd"]],
                    "lag_max": [round(float(v), 2) for v in s["lag_max"]],
                    "drift": [round(float(v), 2) if _has(v) else None for v in s["drift"]],
                    "still_sec": [round(float(v), 1) for v in s["still_sec"]]} for s in homes]
    return res, segs


def timeline(segs, t_start, joint=4, min_abs=0.0):
    """区間ごとの (経過秒, 段, 種類, 止まったあとのずれ, 動いている途中の遅れ, 止まっている間の変化) の一覧."""
    out = []
    for s in segs:
        he = round(float(s["hold_err"][joint]), 2) if _has(s["hold_err"][joint]) else None
        if (he is None or abs(he) < min_abs) and s["lag_max"][joint] < min_abs:
            continue
        out.append({"sec": round(s["t"] - t_start, 1), "stage": s["stage"], "kind": s["kind"],
                    "cmd": round(float(s["cmd"][joint]), 2), "hold_err": he,
                    "lag_max": round(float(s["lag_max"][joint]), 2),
                    "drift": round(float(s["drift"][joint]), 2) if _has(s["drift"][joint]) else None,
                    "still_sec": round(float(s["still_sec"][joint]), 1)})
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--runs-root", default=str(RUNS_ROOT))
    ap.add_argument("--run", action="append", required=True)
    ap.add_argument("--timeline", action="append", default=[], help="区間ごとの一覧を出す run (j4 と j1・j2)")
    ap.add_argument("--json", help="結果を JSON で書く (runs/ の外. 既存のファイルには書かない)")
    args = ap.parse_args()
    root = Path(args.runs_root)
    if str(root).startswith("/Volumes/KIOXIA") and not Path("/Volumes/KIOXIA").is_mount():
        sys.exit("[ABORT] KIOXIA not mounted: /Volumes/KIOXIA")
    out = {"params": {"move_win_sec": MOVE_WIN_SEC, "move_eps_deg": MOVE_EPS_DEG, "event_deg": EVENT_DEG,
                      "long_hold_sec": LONG_HOLD_SEC, "fast_rpc_sec": FAST_RPC_SEC}, "runs": {}, "timelines": {}}
    for stem in args.run:
        res, segs = summarize_run(root / stem)
        out["runs"][stem] = res
        print(f"## {stem}  outcome={res['outcome']} corrections={res['corrections']}")
        if not res["has_send_rows"]:
            print("   send_angles 行なし (依頼 46 より前). log で見る値だけ")
        else:
            print("   粗い最大   " + " ".join(f"{j}={v:5.1f}" for j, v in res["coarse_max"].items()))
            print("   動き途中   " + " ".join(f"{j}={v:5.1f}" for j, v in res["lag_max"].items()))
            def _h(k):          # codex R2 推奨: 止まった区間のない関節は「-」(0.0 と取り違えない)
                return " ".join(f"{j}={'-' if res['hold'][j] is None else format(res['hold'][j][k], '5.1f'):>5}"
                                for j in JOINTS)
            print("   止まり最大 " + _h("max"))
            print("   止まり90%  " + _h("p90"))
            print("   止まり中央 " + _h("median"))
            print("   止まり符号 " + " ".join(f"{j}={'-' if v is None else f'{v:+.1f}':>5}" for j, v in (res["hold_signed_median"] or {}).items()))
            print("   じわじわ   " + " ".join(f"{j}={v:5.1f}" for j, v in res["drift_max"].items()))
            if res["fast_segments"]:
                print("   向かう途中に離れた " + " ".join(f"{j}={v:5.1f}" for j, v in res["away_max"].items())
                      + f"  (すぐ返った区間 {res['fast_segments']}・ゲームパッド {res['pad_segments']})")
            print(f"   段ごと {res['hold_by_context']}")
            print(f"   外れ (>{EVENT_DEG:.0f}°) {res['event_segments']} 区間 ({len(res['events'])} 区間・関節の組)")
            for e in res["events"]:
                print(f"     {e['sec']:6.1f}秒 {e['stage']:7s} {e['kind']:10s} {e['joint']} 指令 {e['cmd']:7.2f} "
                      f"区間の頭 {e['first']:7.2f} → 終わり {e['last']:7.2f}  止まりのずれ {e['hold_err']}  遅れ {e['lag_max']}"
                      + (f"  離れた {e['away_max']}" if "away_max" in e else ""))
            for gr in res["grip"]:
                print(f"   爪 {gr['cmd']} ({gr['stage']}) 前後の変化 {gr['delta']}  爪のあとのずれ {gr['err_after']}")
            for h in res["home"]:
                print(f"   HOME 最後のずれ {h['err_last']}  動き途中 {h['lag_max']}  止まり中の変化 {h['drift']}")
        for k in ("correction", "chunk"):
            st = res["log"][k]
            if st["j4"]:
                print(f"   log {k:10s} n={st['j4']['n']:2d} " + " ".join(
                    f"{j}={st[j]['max']:5.1f}" for j in JOINTS) + "  (最大)")
        if stem in args.timeline and segs:
            t0 = segs[0]["t"]
            tl = {j: timeline(segs, t0, joint=i) for i, j in ((1, "j1"), (2, "j2"), (4, "j4"))}
            out["timelines"][stem] = tl
            print("   区間ごと (j4): 秒 段 種類 指令 止まりのずれ 動き途中 止まり中の変化 止まった秒")
            for e in tl["j4"]:
                print(f"     {e['sec']:6.1f} {e['stage']:7s} {e['kind']:10s} {e['cmd']:7.2f} "
                      f"{'' if e['hold_err'] is None else e['hold_err']:>7} {e['lag_max']:6.2f} "
                      f"{'' if e['drift'] is None else e['drift']:>6} {e['still_sec']:5.1f}")
    if args.json:
        p = Path(args.json)
        if p.exists():
            sys.exit(f"[ABORT] 既にある: {p}")
        # 止まらなかった関節 (NaN) は null にする (JSON に NaN を書かない)
        p.write_text(json.dumps(out, indent=1, ensure_ascii=False, allow_nan=False,
                                default=lambda o: [None if isinstance(x, float) and np.isnan(x) else x for x in o.tolist()]))
        print(f"[JSON] {p}")


if __name__ == "__main__":
    main()
```

### pi05/test_diag_joint_hold.py（テスト24件・commit 284dfd7）

#### pi05/test_diag_joint_hold.py

```python
#!/usr/bin/env python3
"""test_diag_joint_hold.py — diag_joint_hold.py (依頼 57) の確かめ.

  - 動いている途中の遅れと, 止まったあとのずれを分ける (rpc が返ったあとの実測だけを使う)
  - 指令が変わらないのに動いた区間を「外れ」として数える. HOME は数えない
  - 爪の rpc で区間を切る
  - log.jsonl だけでも補正の 1 刻みとチャンクの区切りのずれを出す
  - KIOXIA があれば, 今日の記録で依頼文の粗い値 (j4 の最大) と同じになるか・入れる 3 本に外れがないか
  - 依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行を pad の行と組にする) と, 送ったらすぐ返る daemon の区間
"""
import csv
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

import numpy as np

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))

import diag_joint_hold as d  # noqa: E402

COLS = ["ts", "event_type", "stage", "j0", "j1", "j2", "j3", "j4", "j5", "speed", "gripper_state",
        "ts_send", "ts_arrival", "attempt", "arrived", "note"]


class Rec:
    def __init__(self):
        self.rows = []

    def send(self, t, q, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "send_angles", "stage": stage, **_j(q), "note": f"rpc_sec={rpc}"})

    def rpc(self, t, cmd, stage="correct", rpc=0.03):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd={cmd} rpc_sec={rpc}"})

    def grip(self, t, stage="model", rpc=1.5):
        self.rows.append({"ts": t, "event_type": "rpc", "stage": stage, "note": f"cmd=set_gripper_value rpc_sec={rpc}"})

    def actual(self, t0, t1, q, stage="model", dt=0.1):
        t = t0
        while t < t1 - 1e-9:
            qq = q(t) if callable(q) else q
            self.rows.append({"ts": round(t, 3), "event_type": "actual", "stage": stage, **_j(qq)})
            t += dt

    def write(self, run_dir, log=()):
        rec = run_dir / "recording"
        rec.mkdir(parents=True)
        with (rec / "states.csv").open("w", newline="") as f:
            w = csv.DictWriter(f, fieldnames=COLS)
            w.writeheader()
            for r in self.rows:
                w.writerow({k: r.get(k, "") for k in COLS})
        (run_dir / "log.jsonl").write_text("".join(json.dumps(x) + "\n" for x in log))
        (run_dir / f"{run_dir.name}.json").write_text(json.dumps({"outcome": "success", "corrections": 1}))


def _j(q):
    return {f"j{i}": f"{v:.3f}" for i, v in enumerate(q)}


def pose(j4, j1=10.0):
    return [0.0, j1, 0.0, 0.0, j4, -30.0]


class TestSegments(unittest.TestCase):
    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_x"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_hold_vs_lag_and_events(self):
        r = Rec()
        # 1. 送って rpc が返ったあと, 指令から 1° ずれて止まる (ふつうの止まり)
        r.send(0.0, pose(-50))
        r.actual(1.5, 4.0, pose(-49))
        # 2. 送ったあと 2 秒かけて -60 → -55 へ動き (指令は -60), そのまま止まる (外れ: 指令なしに +5°)
        r.send(4.0, pose(-60))
        r.actual(5.5, 7.5, lambda t: pose(-60 + 5 * min(1.0, (t - 5.5) / 1.0)))
        r.actual(7.5, 9.0, pose(-55))
        # 3. HOME で戻らない (数えない)
        r.send(9.0, pose(0), stage="home")
        r.actual(10.5, 12.0, pose(40), stage="home")
        res, segs = self.build(r)
        self.assertEqual(len(segs), 3)
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 5.0, places=1)       # 止まったあとのずれ (2.)
        self.assertAlmostEqual(res["lag_max"]["j4"], 5.0, places=1)           # 動いている途中 (2. の頭は 0° ずれ… 最後の動きで 5°)
        self.assertEqual(res["hold_by_context"]["model"]["n"], 2)
        ev = res["events"]
        self.assertEqual([(e["sec"], e["joint"]) for e in ev], [(4.0, "j4")])  # HOME は数えない
        self.assertAlmostEqual(ev[0]["hold_err"], 5.0, places=1)
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 40.0, places=1)
        self.assertAlmostEqual(res["coarse_max"]["j4"], 40.0, places=1)       # 粗い値は HOME も含む
        self.assertAlmostEqual(res["hold_signed_median"]["j1"], 0.0, places=2)

    def test_no_samples_during_rpc_and_still_threshold(self):
        r = Rec()
        r.send(0.0, pose(-50), rpc=1.6)
        r.actual(0.0, 1.5, pose(-30))            # rpc の途中の値 (使わない)
        r.actual(1.6, 3.0, pose(-50.2))
        res, segs = self.build(r)
        self.assertEqual(segs[0]["n"], 14)
        self.assertEqual(res["events"], [])
        self.assertAlmostEqual(res["hold"]["j4"]["max"], 0.2, places=2)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_grip_splits_segment(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 2.5, pose(-50))
        r.grip(2.5)
        r.actual(4.0, 5.0, pose(-46))             # 爪のあとで 4° 動いていた (外れの手前)
        res, segs = self.build(r)
        self.assertEqual([s["kind"] for s in segs], ["send", "grip_after"])
        self.assertEqual(res["grip"][0]["delta"][4], 4.0)
        self.assertAlmostEqual(res["hold_by_context"]["grip_after"]["j4_max"], 4.0, places=2)
        self.assertEqual(res["events"], [])        # 4.0 は外れの目安 (4° を超える) に入らない

    def test_drift_while_still(self):
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 6.5, lambda t: pose(-50 + 0.2 * (t - 1.5)))   # 0.2°/秒 でじわじわ (動いているとはしない)
        res, _ = self.build(r)
        self.assertAlmostEqual(res["drift_max"]["j4"], 1.0, delta=0.05)
        self.assertEqual(res["lag_max"]["j4"], 0.0)

    def test_joint_wise_still(self):
        """codex R1 必須: j1 が動き続けていても, 止まった j4 の 5° のずれを見落とさない."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))   # j1 は 2°/秒 で動き続ける. j4 は -55 で止まり
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][1]))                    # j1 は一度も止まっていない
        self.assertAlmostEqual(segs[0]["hold_err"][4], 5.0, places=2)
        self.assertEqual([e["joint"] for e in res["events"]], ["j1", "j4"])
        self.assertEqual(res["event_segments"], 1)                           # 区間の数は 1
        self.assertIsNone(res["hold"]["j1"])

    def test_grip_after_not_still_is_none(self):
        """codex R2 必須: 爪のあとに止まらなかった関節のずれは None (最初の実測のずれで埋めない)."""
        r = Rec()
        r.send(0.0, pose(-50, j1=10))
        r.actual(1.5, 2.5, pose(-50, j1=10))
        r.grip(2.5)
        r.actual(4.0, 6.0, lambda t: pose(-50, j1=10 + 2.0 * (t - 4.0)))
        res, _ = self.build(r)
        g = res["grip"][0]
        self.assertIsNone(g["err_after"][1])
        self.assertEqual(g["err_after"][4], 0.0)
        self.assertEqual(g["err_first"][1], 0.0)

    def test_event_segments_by_index(self):
        """codex R2 推奨: 秒が近い別の区間も別に数える."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.01)
        r.actual(0.01, 0.03, pose(-50))
        r.send(0.04, pose(-60), rpc=0.01)
        r.actual(0.05, 1.0, pose(-50))
        res, _ = self.build(r)
        self.assertEqual(res["event_segments"], 2)

    def test_home_only(self):
        """codex R1 必須: HOME の区間しかない記録でも止まらない."""
        r = Rec()
        r.send(0.0, pose(0), stage="home")
        r.actual(1.5, 3.0, pose(1), stage="home")
        res, _ = self.build(r)
        self.assertEqual(res["work_segments"], 0)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertIsNone(res["hold"]["j4"])
        self.assertAlmostEqual(res["home"][0]["err_last"][4], 1.0, places=2)

    def test_drift_uses_last_still_run(self):
        """codex R1 推奨: 止まり中の変化は最後の止まりが続いたところだけ (途中で動いた分を入れない)."""
        r = Rec()
        r.send(0.0, pose(-50))
        r.actual(1.5, 3.0, pose(-50))
        r.actual(3.0, 3.5, lambda t: pose(-50 + 10 * (t - 3.0)))           # 途中で 5° 動く
        r.actual(3.5, 6.0, pose(-45))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["drift"][4], 0.0, places=2)
        self.assertGreater(segs[0]["still_sec"][4], 1.5)
        self.assertLess(segs[0]["still_sec"][4], 2.5)

    def test_json_has_no_nan(self):
        r = Rec()
        r.send(0.0, pose(-60, j1=10))
        r.actual(1.5, 4.5, lambda t: pose(-55, j1=10 + 2.0 * (t - 1.5)))
        td = Path(tempfile.mkdtemp())
        r.write(td / "pi05_y")
        out = td / "o.json"
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--timeline", "pi05_y",
                                     "--json", str(out)]), mock.patch("sys.stdout"):
            d.main()
        self.assertNotIn("NaN", out.read_text())
        json.loads(out.read_text())
        with mock.patch("sys.argv", ["x", "--runs-root", str(td), "--run", "pi05_y", "--json", str(out)]), \
                mock.patch("sys.stdout"), self.assertRaises(SystemExit):
            d.main()                                                          # 既にあるファイルには書かない

    def test_log_hold_errors(self):
        log = [
            {"chunk": 0, "t": 1.0, "executed": True, "state_source": "measured",
             "measured_state": pose(-1) + [0.0], "sent_targets": [pose(-40)]},
            {"chunk": 1, "t": 2.0, "executed": False, "state_source": "target",
             "measured_state": pose(-37) + [0.0], "sent_targets": [pose(-45)]},
            {"type": "correction", "action": "move", "sent": True, "t": 3.0,
             "after": pose(-45), "measured_after": pose(-30)},
            {"type": "rewind", "t": 4.0},
            {"chunk": 2, "t": 5.0, "executed": True, "state_source": "measured", "measured_state": pose(9) + [0.0]},
        ]
        out = d.log_hold_errors(log)
        self.assertEqual([round(float(e[4]), 2) for _, e in out["chunk"]], [3.0])     # 実行していないチャンクの目標は使わない. 巻き戻しのあとは比べない
        self.assertEqual([round(float(e[4]), 2) for _, e in out["correction"]], [15.0])

    def test_old_record_without_send_rows(self):
        r = Rec()
        r.actual(0.0, 2.0, pose(-50))
        res, segs = self.build(r)
        self.assertFalse(res["has_send_rows"])
        self.assertEqual(segs, [])


def pad_move(t, command, joint, hold=False, resends=(), cid=1):
    r = {"type": "pad", "correction_id": cid, "kind": "move", "sent": True, "t_send": t, "command": command,
         "joint": joint, "resends": list(resends)}
    if hold:
        r["no_check"] = True
    return r


def pad_halt(t, q, joint, cid=1):
    return {"type": "pad", "correction_id": cid, "kind": "halt", "t_halt": t, "measured_after_halt": q,
            "halt_joint": joint, "halt": "stop"}


class TestPadAndFast(unittest.TestCase):
    """依頼 97: ゲームパッドの補正 (send_angle / stop の rpc 行) と, 送ったらすぐ返る daemon の区間."""

    def build(self, rec, log=()):
        td = tempfile.mkdtemp()
        run = Path(td) / "pi05_p"
        rec.write(run, log)
        return d.summarize_run(run)

    def test_fast_approach_is_not_lag(self):
        """rpc がすぐ返ると, 腕は返ったあとに指令へ向かって動く. 途中で一度止まってまた向かっても外れにしない."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.6, pose(-50))                                         # 動き出す前 (前の姿勢のまま)
        r.actual(0.6, 1.1, lambda t: pose(-50 - 10 * (t - 0.6)))               # -55 まで
        r.actual(1.1, 1.8, pose(-55))                                          # 一度止まる
        r.actual(1.8, 2.3, lambda t: pose(-55 - 9 * (t - 1.8)))                # また向かう
        r.actual(2.3, 3.5, pose(-59.5))
        res, segs = self.build(r)
        self.assertEqual(res["fast_segments"], 1)
        self.assertEqual(res["events"], [])
        self.assertEqual(res["lag_max"]["j4"], 0.0)
        self.assertAlmostEqual(segs[0]["hold_err"][4], 0.5, places=2)
        self.assertLess(res["away_max"]["j4"], 0.2)

    def test_fast_hold_ignores_still_before_moving(self):
        """すぐ返った区間で, 動き出す前の止まり (前の姿勢) を止まったあとのずれにしない (動き続けたら None)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 0.8, pose(-50))
        r.actual(0.8, 2.0, lambda t: pose(-50 - 8 * (t - 0.8)))
        res, segs = self.build(r)
        self.assertTrue(np.isnan(segs[0]["hold_err"][4]))
        self.assertEqual(res["events"], [])

    def test_fast_sag_after_arrival_is_event(self):
        """すぐ返った区間でも, 着いたあとに指令なしで 6° 垂れたら外れ (away と止まりのずれ)."""
        r = Rec()
        r.send(0.0, pose(-60), rpc=0.03)
        r.actual(0.03, 1.0, pose(-60))
        r.actual(1.0, 1.6, lambda t: pose(-60 + 10 * (t - 1.0)))
        r.actual(1.6, 3.0, pose(-54))
        res, segs = self.build(r)
        self.assertAlmostEqual(segs[0]["away_max"][4], 6.0, delta=0.05)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        self.assertAlmostEqual(res["events"][0]["away_max"], 6.0, delta=0.05)

    def test_pad_step_hold_stop(self):
        """1 刻み: 指令は pad の command. 長押し: 押した関節は見ない. stop: 止めた関節だけ止めたあとの角度."""
        start = pose(-60, j1=40)
        step = pose(-60, j1=40); step[0] = 1.0
        far = pose(-60, j1=25); far[0] = 1.0                                   # j1 を 15° 先へ (ほかは直前の指令)
        r = Rec()
        r.send(0.0, start, stage="model", rpc=0.03)
        r.actual(0.03, 1.0, start, stage="model")
        r.rpc(2.0, "send_angle")                                               # 1 刻み (j0 +1)
        r.actual(2.03, 3.0, step, stage="correct")
        r.rpc(3.0, "send_angle")                                               # 長押し (j1 → 25)
        r.actual(3.03, 3.5, lambda t: [1.0, 40 - 20 * (t - 3.03), 0, 0, -60, -30], stage="correct")
        r.rpc(3.5, "stop")
        r.actual(3.55, 5.0, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
        r.rpc(5.0, "send_angle")                                               # 組にできない (pad の行がない… 送り直しの数を越える)
        r.actual(5.03, 5.5, [1.0, 30.6, 0, 0, -60, -30], stage="correct")
        log = [pad_move(2.0, step, 0), pad_move(3.0, far, 1, hold=True), pad_halt(3.5, [1.0, 30.6, 0, 0, -60.2, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_step", "pad_hold", "pad_stop", "pad_unknown"])
        self.assertEqual(res["pad_segments"], {"pad_step": 1, "pad_hold": 1, "pad_stop": 1, "pad_unknown": 1})
        np.testing.assert_allclose(segs[1]["cmd"], step)
        self.assertTrue(np.isnan(segs[2]["hold_err"][1]))                     # 長押しの関節は見ない
        self.assertEqual(segs[2]["lag_max"][1], 0.0)
        self.assertEqual(segs[2]["skip"], 1)
        np.testing.assert_allclose(segs[3]["cmd"], [1.0, 30.6, 0, 0, -60, -30])   # j4 は止めたあとの -60.2 にしない
        self.assertTrue(np.isnan(segs[4]["cmd"]).all())
        self.assertEqual(res["events"], [])

    def test_pad_resend_and_uncommanded_wrist(self):
        """送り直し (resends) は同じ pad の行に組む. 長押しの間に手首が指令なしで 6° 動いたら stop の区間で外れ."""
        start = pose(-60, j1=50)
        far = pose(-60, j1=35)
        r = Rec()
        r.send(0.0, start, rpc=0.03)
        r.actual(0.03, 1.0, start)
        r.rpc(2.0, "send_angle")
        r.rpc(2.3, "send_angle")                                               # 送り直し
        r.actual(2.33, 3.0, lambda t: [0, 50 - 15 * (t - 2.33), 0, 0, -60 + 9 * (t - 2.33), -30], stage="correct")
        r.rpc(3.0, "stop")
        r.actual(3.05, 4.5, [0, 40, 0, 0, -54, -30], stage="correct")
        log = [pad_move(2.0, far, 1, hold=True, resends=[0.3]), pad_halt(3.0, [0, 40, 0, 0, -54, -30], 1)]
        res, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_hold", "pad_hold", "pad_stop"])
        self.assertEqual(res["pad_segments"]["pad_unknown"], 0)
        self.assertEqual([(e["kind"], e["joint"]) for e in res["events"]], [("pad_hold", "j4"), ("pad_stop", "j4")])
        self.assertAlmostEqual(segs[3]["hold_err"][4], 6.0, places=2)

    def test_pad_send_not_matched_to_next(self):
        """codex R1 必須: rpc の時刻に近い送信と組にする (0.15 秒あとの次の送信を拾わない)."""
        a_cmd = pose(-60, j1=40); a_cmd[0] = 1.0
        b_cmd = pose(-60, j1=40); b_cmd[0] = 2.0
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "send_angle")
        r.actual(1.03, 1.14, a_cmd, stage="correct")
        r.rpc(1.16, "send_angle")
        r.actual(1.19, 2.0, b_cmd, stage="correct")
        log = [pad_move(1.0, a_cmd, 0), pad_move(1.15, b_cmd, 0)]
        _, segs = self.build(r, log)
        np.testing.assert_allclose(segs[1]["cmd"], a_cmd)
        np.testing.assert_allclose(segs[2]["cmd"], b_cmd)

    def test_away_carries_across_fast_segments(self):
        """codex R1 必須: j4 の指令が同じまま, ほかの関節の送信で区間が切れても, 0→3° と 3→6° の垂れを 6° と見る."""
        r = Rec()
        r.send(0.0, pose(-60, j1=10), rpc=0.03)
        r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t, j1=10))
        r.send(1.0, pose(-60, j1=11), rpc=0.03)          # j1 だけ変わる (j4 の指令は同じ)
        r.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t, j1=11))
        res, segs = self.build(r)
        self.assertLess(segs[0]["away_max"][4], 3.1)
        self.assertGreater(segs[1]["away_max"][4], 5.5)
        self.assertEqual([e["joint"] for e in res["events"]], ["j4"])
        r2 = Rec()                                          # j4 の指令が変われば引き継がない
        r2.send(0.0, pose(-60), rpc=0.03)
        r2.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
        r2.send(1.0, pose(-57), rpc=0.03)
        r2.actual(1.03, 2.0, lambda t: pose(-60 + 3 * t))
        res2, _ = self.build(r2)
        self.assertEqual(res2["events"], [])

    def test_carry_cut_by_segments_without_samples(self):
        """codex R2 必須: 実測のない区間 (指令が変わる・すぐ返らない・長押し・組にできない) を挟めば引き継がない.
        指令 0°・実測 0→3° → (実測なしの区間) → 指令 0°・実測 3→6° で, 最後の away は 3° (6° にしない)."""
        def run(between, log=()):
            r = Rec()
            r.send(0.0, pose(-60), rpc=0.03)
            r.actual(0.03, 1.0, lambda t: pose(-60 + 3 * t))
            between(r)
            r.send(1.2, pose(-60), rpc=0.03)
            r.actual(1.23, 2.2, lambda t: pose(-60 + 3 * (t - 0.2)))
            _, segs = self.build(r, log)
            return segs[-1]["away_max"][4]
        self.assertLess(run(lambda r: r.send(1.0, pose(-50), rpc=0.03)), 3.1)              # 指令が変わる
        self.assertLess(run(lambda r: r.send(1.0, pose(-60), rpc=1.5)), 3.1)               # すぐ返らない
        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model")), 3.1)       # 組にできない
        far = pose(-45)
        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model"),
                            [pad_move(1.0, far, 4, hold=True)]), 3.1)                        # j4 の長押し
        self.assertGreater(run(lambda r: r.grip(1.0, rpc=0.01)), 5.5)                      # 爪だけ: 引き継ぐ
        self.assertLess(run(lambda r: r.rpc(1.0, "send_angle", stage="model"),
                            [pad_move(1.0, pose(-60), 4, hold=True)]), 3.1)                  # 同じ指令のまま j4 の長押し (codex R3)

    def test_halt_one_to_one(self):
        """codex R1 推奨: stop はいちばん近い・まだ組にしていない halt と組む (同じ halt を 2 回使わない)."""
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "stop", stage="correct")
        r.actual(1.03, 1.1, pose(-60, j1=35), stage="correct")
        r.rpc(1.15, "stop", stage="correct")
        r.actual(1.18, 2.0, pose(-60, j1=30), stage="correct")
        log = [pad_halt(1.0, pose(-60, j1=35), 1), pad_halt(1.15, pose(-60, j1=30), 1)]
        _, segs = self.build(r, log)
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_stop", "pad_stop"])
        self.assertAlmostEqual(segs[1]["cmd"][1], 35.0)
        self.assertAlmostEqual(segs[2]["cmd"][1], 30.0)

    def test_halt_not_reused(self):
        """codex R3 推奨: halt が 1 件で stop が 2 件なら, 2 件目は pad_unknown (同じ halt を使わない)."""
        r = Rec()
        r.send(0.0, pose(-60, j1=40), rpc=0.03)
        r.actual(0.03, 0.9, pose(-60, j1=40))
        r.rpc(1.0, "stop", stage="correct")
        r.actual(1.03, 1.08, pose(-60, j1=35), stage="correct")
        r.rpc(1.1, "stop", stage="correct")
        r.actual(1.13, 2.0, pose(-60, j1=35), stage="correct")
        _, segs = self.build(r, [pad_halt(1.05, pose(-60, j1=35), 1)])
        self.assertEqual([s["kind"] for s in segs], ["send", "pad_stop", "pad_unknown"])


RUNS = Path("/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")


@unittest.skipUnless(RUNS.is_dir(), "KIOXIA がない")
class TestToday(unittest.TestCase):
    """依頼文の粗い値 (j4 の「直前に送った指令と実測の差」の最大) と同じになり, 入れる 3 本には外れがない."""
    COARSE = {"190347": 3.5, "190854": 2.5, "192101": 1.8, "192418": 10.3, "192758": 19.5,
              "193041": 18.4, "193411": 8.0, "194042": 59.5}

    def test_coarse_and_events(self):
        for stem, v in self.COARSE.items():
            res, _ = d.summarize_run(RUNS / f"pi05_20261101_{stem}")
            self.assertAlmostEqual(res["coarse_max"]["j4"], v, delta=0.051, msg=stem)
            n_ev = len([e for e in res["events"] if e["joint"] == "j4"])
            if stem in ("190347", "190854", "192101"):
                self.assertEqual(n_ev, 0, stem)
            else:
                self.assertGreater(n_ev, 0, stem)

    def test_20261106_gamepad(self):
        """依頼 97: 11-06 の成功 5 本. rpc 行はすべて pad の行と組になり, 外れは 185554 (長押しの間に j4 +6.8°) だけ."""
        for stem in ("183926", "184452", "184650", "184906", "185554"):
            p = RUNS / f"pi05_20261106_{stem}"
            if not p.is_dir():
                self.skipTest("11-06 の記録がない")
            res, _ = d.summarize_run(p)
            self.assertEqual(res["pad_segments"]["pad_unknown"], 0, stem)
            self.assertGreater(res["fast_segments"], 0, stem)
            if stem == "185554":
                self.assertEqual({e["joint"] for e in res["events"]}, {"j4"})
                self.assertAlmostEqual(res["hold"]["j4"]["max"], 6.77, delta=0.051)
            else:
                self.assertEqual(res["events"], [], stem)


if __name__ == "__main__":
    unittest.main()
```

### pi05/test_pad_build.py（テスト3件・commit 284dfd7）

#### pi05/test_pad_build.py

```python
#!/usr/bin/env python3
"""test_pad_build.py — ゲームパッドの補正 (run_policy_client の p. cd2fb46) の記録から
build_correction_episodes.py が学習データを作れるかの確かめ (2026-11-06 依頼 97).

  python3 pi05/test_pad_build.py -v

合成の記録だけ (KIOXIA にもアームにも触らない). 確かめること:
  - 補正の間 (状態 correct) のコマが学習に入る. 長押しで動いている途中のコマも入る
  - 補正の中で送った爪の指令 (recorder.gripper → states.csv の gripper_cmd 行) の時刻で 0/1 が切り替わる
  - 補正を始めたときの約 3 秒の抜け (ゲームパッドの窓を開く間. 腕は止まっている) と, 長押しの途中の 0.3 秒の抜けは,
    speed20 で埋めても linear と同じ (抜けが短く, 速度 20 の速さでは抜けいっぱいを使う)
  - 長押しを離して stop で止まったところは, 今までと同じ決まり (--trim-hold. correct は 1 コマ) で詰まる
"""
import argparse
import json
import sys
import tempfile
import unittest
from pathlib import Path

import cv2
import numpy as np

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))

import build_correction_episodes as bce  # noqa: E402

COLS = ["ts", "event_type", "stage", "j0", "j1", "j2", "j3", "j4", "j5", "speed", "gripper_state",
        "ts_send", "ts_arrival", "attempt", "arrived", "note"]
BASE = 1.8e9


def pose_at(t):
    """合成の動き (秒 → 6 関節).
    0〜2 秒 model: j0 0 → 10 / 2〜5 秒 correct: 止まり (ゲームパッドの窓を開く間. 実測なし)
    5.0〜5.6 秒 correct: j1 を長押しで 10 → 46 (60°/秒. 5.2〜5.5 秒は実測なし) / 5.6 秒 stop
    5.6〜9 秒 correct: 止まり (8.0 秒に爪を閉じる指令) / 9〜11 秒 model: j0 10 → 20"""
    j0 = 10.0 * min(t, 2.0) / 2.0 + (10.0 * (t - 9.0) / 2.0 if t > 9.0 else 0.0)
    j1 = 10.0 + 60.0 * min(max(t - 5.0, 0.0), 0.6)
    return [j0, j1, 5.0, 0.0, -40.0, -30.0]


def sample_times():
    t = np.round(np.arange(0.0, 11.0, 0.1), 3)
    gap = ((t > 2.05) & (t < 5.0)) | ((t > 5.15) & (t < 5.5))
    return t[~gap]


def write_run(run_dir, size=(640, 360), fps_cam=10.0):
    rec = run_dir / "recording"
    rec.mkdir(parents=True)
    rows = [(0.0, "stage", "model", None, None, ""), (2.0, "stage", "correct", None, None, ""),
            (9.0, "stage", "model", None, None, ""),
            (0.5, "gripper_cmd", "model", None, 0, ""),
            (8.0, "gripper_cmd", "correct", None, 1, ""),                        # 補正の中の爪 (recorder.gripper)
            (8.0001, "rpc", "correct", None, None, "cmd=set_gripper_value rpc_sec=0.013"),
            (5.0, "rpc", "correct", None, None, "cmd=send_angle rpc_sec=0.034"),
            (5.6, "rpc", "correct", None, None, "cmd=stop rpc_sec=0.025")]
    for t in sample_times():
        rows.append((float(t) + 0.01, "actual", "model" if t < 2.0 or t >= 9.0 else "correct", pose_at(t), None, ""))
    rows.sort(key=lambda r: r[0])
    with (rec / "states.csv").open("w") as f:
        f.write(",".join(COLS) + "\n")
        for ts, ev, mode, ang, gs, note in rows:
            angs = ["" if ang is None else f"{v:.3f}" for v in (ang or [None] * 6)]
            f.write(",".join([f"{BASE + ts:.6f}", ev, mode] + angs + ["", "" if gs is None else str(gs)]
                             + [""] * 4 + [note]) + "\n")
    w = cv2.VideoWriter(str(rec / "top.mp4"), cv2.VideoWriter_fourcc(*"mp4v"), fps_cam, size)
    with (rec / "top_frame_log.csv").open("w") as f:
        f.write("frame_idx,capture_ts,mode\n")
        for k, tt in enumerate(np.arange(0, 11.0, 1.0 / fps_cam)):
            w.write(np.full((size[1], size[0], 3), 20 + (k % 40), np.uint8))
            f.write(f"{k},{BASE + tt:.6f},model\n")
    w.release()
    (rec / "top_meta.json").write_text(json.dumps({"status": "ok"}))
    (run_dir / f"{run_dir.name}.json").write_text(json.dumps({
        "recording_enabled": True, "result": "completed", "outcome": "success",
        "judgments": {"pick": {"human_verdict": "yes"}, "place": {"human_verdict": "yes"}}}))
    log = [{"type": "pad", "correction_id": 1, "kind": "move", "sent": True, "t_send": BASE + 5.0, "joint": 1,
            "no_check": True, "command": [10, 25, 5, 0, -40, -30]},
           {"type": "pad", "correction_id": 1, "kind": "halt", "t_halt": BASE + 5.6, "halt": "stop", "halt_joint": 1,
            "measured_after_halt": [10, 46, 5, 0, -40, -30]},
           {"type": "pad", "correction_id": 1, "kind": "grip_close", "sent": True, "grip_cmd": 1},
           {"type": "correction", "action": "accept", "device": "gamepad", "correction_id": 1, "after_chunk": 0,
            "t_start": BASE + 2.0, "t_end": BASE + 9.0, "grip_used": True}]
    (run_dir / "log.jsonl").write_text("".join(json.dumps(x) + "\n" for x in log))


class TestPadBuild(unittest.TestCase):
    def build(self, gap_fill):
        td = Path(tempfile.mkdtemp())
        run = td / "runs" / "pi05_20991106_000000"
        write_run(run)
        out = td / "ds" / "raw_episodes" / run.name
        args = argparse.Namespace(force=True, no_trim_mid_idle=False, gap_fill=gap_fill, trim_hold=True)
        res = bce.build_one(run, out, args)
        self.assertTrue(res["used"], res)
        z = np.load(out / "episode.npz")
        meta = json.loads((out / "meta.json").read_text())
        return z, meta

    def test_correct_steps_and_gripper(self):
        z, meta = self.build("speed20")
        st, modes, t = z["state"], meta["step_modes"], z["t_orig"]
        self.assertGreater(meta["correct_steps"], 0)
        # 長押しで動いている途中のコマ (j1 が 10 と 46 の間) が correct として入る
        mid = [i for i in range(len(st)) if 12.0 < st[i, 1] < 44.0]
        self.assertGreaterEqual(len(mid), 3)
        self.assertTrue(all(modes[i] == "correct" for i in mid))
        # 爪は補正の中の指令の時刻で 0 → 1 に切り替わる (切り替わる前後のコマは correct)
        flip = int(np.flatnonzero(np.diff(st[:, 6]) != 0)[0])
        self.assertEqual(st[flip, 6], 0.0)
        self.assertEqual(st[flip + 1, 6], 1.0)
        self.assertEqual(modes[flip + 1], "correct")
        self.assertEqual(int(np.sum(np.diff(st[:, 6]) != 0)), 1)
        self.assertTrue(np.all(np.diff(t) > 0))

    def test_stop_after_release_is_trimmed(self):
        z, meta = self.build("speed20")
        st, a, modes = z["state"], z["actions"], meta["step_modes"]
        # 止まり = 関節が動かず爪も切り替わらないコマ (check_dataset と同じ. 爪を切り替えるコマは数えない)
        still = (np.max(np.abs(a[:, :6] - st[:, :6]), axis=1) < 0.3) & (a[:, 6] == st[:, 6])
        run, longest = 0, 0
        for i in range(len(st)):
            run = run + 1 if still[i] and modes[i] == "correct" else 0
            longest = max(longest, run)
        # 長押しを離したあとの止まり (5.6〜9 秒) は, 爪の切り替えの直前・直後の 2 コマずつ (trim2 と同じ) まで
        self.assertLessEqual(longest, 2)
        held = [i for i in range(len(st)) if abs(st[i, 1] - 46.0) < 1e-3 and modes[i] == "correct"]
        self.assertLessEqual(len(held), 2 + 2 + 1)        # correct の 1 + 爪の切り替えの前後 (keep_grip 2 + guard)

    def test_speed20_same_as_linear_for_pad_gaps(self):
        z1, m1 = self.build("speed20")
        z2, m2 = self.build("linear")
        np.testing.assert_allclose(z1["state"], z2["state"])
        self.assertEqual(m1["step_modes"], m2["step_modes"])
        self.assertEqual(m1["gap_fill"]["gaps"], 2)                 # 窓を開く間の 3 秒と, 長押しの途中の 0.4 秒
        self.assertEqual(m1["gap_fill"]["motion_gaps"], 1)


if __name__ == "__main__":
    unittest.main()
```

### pi05/openpi_addon/add_trim_corr_config.py（commit 284dfd7）

#### pi05/openpi_addon/add_trim_corr_config.py

```python
#!/usr/bin/env python3
"""openpi config.py に trim2 + 補正データ用の学習設定 pi05_mecharm_trim_corr_lora を足す (2026-10-31 依頼 45).

依頼 46: データを corr_v2 (角度の抜けを速度 20 の動きで埋め直したもの) と合わせた
mecharm_redblock_box_teach_v1_trim2_corr_v2 に替えた. corr_v1 の設定が入っている config.py には足さずに止まる
(同じ名前の設定が 2 つになるため. openpi の config.py を git checkout してから入れ直す).
依頼 48: データを corr_v3 (着いたあとの止まりを詰めたもの) と合わせた ..._trim2_corr_v3 に替え,
学習率の予定を続きからの学習用に弱めた (warmup 100 / peak 2.5e-5 / decay_steps 1_500. 下げきる先 decay_lr 5e-6 は
trim2 と同じ). corr_v1 / corr_v2 の設定が入っている config.py には足さずに止まる.

install_addon.py と add_trim_config.py (lab/Log11/pod/) を実行した後に 1 回実行:
  python3 add_trim_corr_config.py /workspace/openpi --steps 4000 --init base
  python3 add_trim_corr_config.py /workspace/openpi --steps 1500 --init /workspace/ckpt/3999/params

pi05_mecharm_trim_lora の設定をそのまま写し, データだけを替える:
  name             pi05_mecharm_trim_lora                     -> pi05_mecharm_trim_corr_lora
  repo_id          local/mecharm_redblock_box_teach_v1_trim2  -> local/mecharm_redblock_box_teach_v1_trim2_corr_v2
  num_train_steps  4_000                                      -> --steps (決めるのはエイイチ. 既定値は置かない)
  lr_schedule      warmup 300 / peak 5e-5 / decay_steps 5_000 -> warmup 100 / peak 2.5e-5 / decay_steps 1_500
                   (decay_lr 5e-6 はそのまま. 依頼 48)
  weight_loader    pi05_base                                  -> --init base ならそのまま (一から)
                                                                 --init <params> なら trim2 の学習済みから続ける
batch 32 などほかは trim2 と同じ. 正規化の値は計算し直さず trim2 の値を置く (pod_prep_trim_corr.sh).
何度実行しても同じ結果 (二重に入れない). 入っている設定と --steps / --init が違うときは止まる.

依頼 57: --variant v4 で, corr_v4 (今日の手首の外れのなかった 3 本を足したもの) と合わせた ..._trim2_corr_v4 の設定
pi05_mecharm_trim_corr_v4_lora を足す (名前が違うので v3 の設定と並べて入る). 学習率の予定・ほかの値は v3 と同じ.
続きの元 (--init) は corr_v1_1499 (/workspace/ckpt/corr_v1_1499/params). 既定は --variant v3 (今までと同じ)
  python3 add_trim_corr_config.py /workspace/openpi --variant v4 --steps 1500 --init /workspace/ckpt/corr_v1_1499/params
依頼 58: v4 のデータを ..._trim2_corr_v4b (corr_v4 から 10-31 の 182545 を外した 7 本) に替えた. 設定の名前は
pi05_mecharm_trim_corr_v4_lora のまま. 8 本の ..._trim2_corr_v4 の設定が入っている config.py には足さずに止まる
依頼 97: --variant v5 で, corr_v5 (corr_v4b の 7 本 + 11-06 のゲームパッドで直した 4 本) と合わせた ..._trim2_corr_v5 の設定
pi05_mecharm_trim_corr_v5_lora を足す (v3・v4 と並べて入る). 学習率の予定・ほかの値は v3 と同じ.
続きの元 (--init) は corr_v2_1499 (/workspace/ckpt/corr_v2_1499/params)
  python3 add_trim_corr_config.py /workspace/openpi --variant v5 --steps 1500 --init /workspace/ckpt/corr_v2_1499/params
"""
import argparse
import re
import sys
from pathlib import Path

# 版ごとの 設定の名前・データ. old_tags = 入っていたら止まる前の版 (同じ設定の名前を使っていたもの)
VARIANTS = {
    "v3": {"name": "pi05_mecharm_trim_corr_lora", "data_tag": "teach_v1_trim2_corr_v3",
           "old_tags": ("teach_v1_trim2_corr_v1", "teach_v1_trim2_corr_v2")},   # 依頼 45 / 46 で入れた版
    "v4": {"name": "pi05_mecharm_trim_corr_v4_lora", "data_tag": "teach_v1_trim2_corr_v4b",
           "old_tags": ("teach_v1_trim2_corr_v4",)},   # 依頼 57 / 58 (8 本の corr_v4 → 182545 を外した 7 本の corr_v4b)
    "v5": {"name": "pi05_mecharm_trim_corr_v5_lora", "data_tag": "teach_v1_trim2_corr_v5", "old_tags": ()},  # 依頼 97
}


def variant(v):
    """版 v の (NAME, REPO_ID, OLD_TAGS, MARK_A, MARK_B)."""
    d = VARIANTS[v]
    tag = d["data_tag"]
    return (d["name"], f"local/mecharm_redblock_box_{tag}", d["old_tags"],
            f"    # >>> r2r2r mecharm {tag} (train config)\n", f"    # <<< r2r2r mecharm {tag} (train config)\n")


NAME, REPO_ID, OLD_TAGS, MARK_A, MARK_B = variant("v3")      # 既定の版 (今までと同じ)
# 学習率の予定 (依頼 48. 3999 からの続きの学習 1,500 ステップ用)
LR_REPS = [
    ("warmup_steps=300,", "warmup_steps=100,"),
    ("peak_lr=5e-5,", "peak_lr=2.5e-5,"),
    ("decay_steps=5_000,", "decay_steps=1_500,"),
]
DECAY_LR = "decay_lr=5e-6,"     # 下げきる先. trim2 と同じ (変えない)
BASE_LOADER = 'weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"),'


def make_block(s, steps, init, v="v3"):
    name, repo_id, _, mark_a, mark_b = variant(v)
    A = "    # >>> r2r2r mecharm teach_v1_trim2 (train config)\n"
    B = "    # <<< r2r2r mecharm teach_v1_trim2 (train config)\n"
    assert s.count(A) == 1 and s.count(B) == 1, "先に install_addon.py と add_trim_config.py を実行する"
    block = s[s.index(A):s.index(B) + len(B)]
    reps = [
        (A, mark_a),
        (B, mark_b),
        ('name="pi05_mecharm_trim_lora"', f'name="{name}"'),
        ('repo_id="local/mecharm_redblock_box_teach_v1_trim2"', f'repo_id="{repo_id}"'),
        ("num_train_steps=4_000,", f"num_train_steps={steps},"),
        *LR_REPS,
    ]
    if init != "base":
        reps.append((BASE_LOADER, f'weight_loader=weight_loaders.CheckpointWeightLoader("{init}"),'))
    for a, b in reps:
        assert block.count(a) == 1, f"見つからない/重複: {a!r}"
        block = block.replace(a, b)
    assert block.count(DECAY_LR) == 1, "trim2 の下げきる先 (decay_lr) が見つからない"
    return block


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("openpi_root")
    ap.add_argument("--variant", choices=sorted(VARIANTS), default="v3",
                    help="v3 = pi05_mecharm_trim_corr_lora (..._trim2_corr_v3. 既定) / "
                         "v4 = pi05_mecharm_trim_corr_v4_lora (..._trim2_corr_v4b. 依頼 57 / 58)")
    ap.add_argument("--steps", type=int, required=True, help="学習のステップ数 (num_train_steps)")
    ap.add_argument("--init", required=True,
                    help="base = π0.5 base から一から / それ以外 = 続きから学習する params のパス "
                         "(例 /workspace/ckpt/3999/params)")
    args = ap.parse_args(argv)
    if args.steps <= 0:
        sys.exit("[ERROR] --steps は 1 以上")
    if args.init != "base" and not args.init.rstrip("/").endswith("params"):
        sys.exit(f"[ERROR] --init には checkpoint の params フォルダを渡す: {args.init}")
    cfg = Path(args.openpi_root).resolve() / "src" / "openpi" / "training" / "config.py"
    name, repo_id, old_tags, mark_a, mark_b = variant(args.variant)
    s = cfg.read_text()
    old = [t for t in old_tags if f"    # >>> r2r2r mecharm {t} (train config)\n" in s]
    if old:
        sys.exit(f"[ERROR] 前の版 ({', '.join(old)}) の {name} が入っている. "
                 "openpi の config.py を git checkout してから入れ直す")
    if mark_a in s:
        have = s[s.index(mark_a):]
        have = have[:have.index(mark_b)]
        m = re.search(r"num_train_steps=(\d+),", have)
        same_steps = m and int(m.group(1)) == args.steps
        same_init = (BASE_LOADER in have) if args.init == "base" else (f'("{args.init}")' in have)
        # codex 推奨 (依頼 48): データ名・学習率の予定も入っているものと同じかを見る
        same_rest = (f'repo_id="{repo_id}"' in have and all(b in have for _, b in LR_REPS)
                     and DECAY_LR in have)
        if not same_rest:
            sys.exit("[ERROR] 入っている設定のデータ名・学習率の予定が違う. openpi の config.py を git checkout してから入れ直す")
        if not (same_steps and same_init):
            sys.exit("[ERROR] 別の --steps / --init で入っている. openpi の config.py を git checkout してから入れ直す")
        print("already installed (config.py は変更なし)")
        return 0
    if f'name="{name}"' in s:
        sys.exit(f"[ERROR] {name} がもう別の形で入っている. openpi の config.py を git checkout してから入れ直す")
    block = make_block(s, args.steps, args.init, args.variant)
    B = "    # <<< r2r2r mecharm teach_v1_trim2 (train config)\n"
    j = s.index(B) + len(B)
    cfg.write_text(s[:j] + block + s[j:])
    print(f"installed: {name} (repo_id={repo_id}, {args.steps} steps, init={args.init})")
    return 0


if __name__ == "__main__":
    sys.exit(main())
```

### pi05/openpi_addon/pod_prep_trim_corr.sh（commit 284dfd7）

#### pi05/openpi_addon/pod_prep_trim_corr.sh

```bash
#!/usr/bin/env bash
# pod_prep_trim_corr.sh — Pod で trim2 + corr_v3 の学習の準備をする (2026-10-31 依頼 46 / 48). 学習は回さない.
#
# 前提: lab_start.sh で Pod を準備してある (openpi・install_addon.py・add_trim_config.py・/workspace/ckpt/3999)
#       Mac から次の 2 つを送ってある:
#         /workspace/data/mecharm_redblock_box_teach_v1_trim2_corr_v3_raw_episodes.tar
#         /workspace/r2r2r/pi05/  (この依頼の版. add_trim_corr_config.py と convert_to_lerobot.py を使う)
#
#   bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh <steps> <init> [v3|v4|v5]
#   例: bash ... 1500 /workspace/ckpt/3999/params
#   依頼 57: 3 つ目に v4 を付けると trim2 + corr_v4 (..._trim2_corr_v4・設定 pi05_mecharm_trim_corr_v4_lora) の準備.
#     例: bash ... 1500 /workspace/ckpt/corr_v1_1499/params v4
#     続きの元は corr_v1_1499 (lab_train.sh --model corr2 の 3c が取る). 3999 は取らないので, 正規化の値は
#     1499 の assets にある値 (3999 の trim2 の値と sha256 が同じ. 依頼 52 で確かめた) を, 同じ sha256 で確かめて置く
#   依頼 58: v4 のデータは ..._trim2_corr_v4b (8 本の corr_v4 から 182545 を外した 7 本. 合わせて 26 本). 設定の名前は同じ
#   依頼 97: v5 は trim2 + corr_v5 (..._trim2_corr_v5・設定 pi05_mecharm_trim_corr_v5_lora. 合わせて 30 本).
#     続きの元は corr_v2_1499 (lab_train.sh --model corr3 の 3c が取る). 正規化の値は 1499 の assets にある値
#     (trim2 の値と sha256 が同じ. 依頼 97 で KIOXIA の写しを確かめた) を, 同じ sha256 で確かめて置く
#     例: bash ... 1500 /workspace/ckpt/corr_v2_1499/params v5
#   v3 (既定) は今までと同じ
#
# すること (何度流しても同じ結果):
#   1. tar を /workspace/data/<データ名>/ に広げ, エピソードの本数を出す
#   2. openpi の config.py に pi05_mecharm_trim_corr_lora を足す (--steps / --init)
#   3. LeRobot 形式に変換 (repo_id local/<データ名>)
#   4. 正規化の値は計算し直さず, 3999 の学習で使った trim2 の値をそのまま置く (依頼 48)
#      元: 3999 のモデルの assets (lab_start.sh 3c の hf download が置く)
#        /workspace/ckpt/3999/assets/local/mecharm_redblock_box_teach_v1_trim2/norm_stats.json
#      先: openpi が学習で読む場所 (assets/<設定の名前>/<repo_id>/)
#        assets/pi05_mecharm_trim_corr_lora/local/<データ名>/norm_stats.json
#      Mac の写し (/Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_lora_v1/3999/assets/...) と
#      sha256 が同じかを確かめる
#   5. 学習を始めるコマンドを出す (流さない)
set -euo pipefail

STEPS="${1:?学習のステップ数 (例 1500)}"
INIT="${2:?続きから学習する params (例 /workspace/ckpt/3999/params) / 一からなら base}"
VARIANT="${3:-v3}"
case "$VARIANT" in
    v3) NAME=mecharm_redblock_box_teach_v1_trim2_corr_v3
        CONFIG=pi05_mecharm_trim_corr_lora
        NORM_SRC=/workspace/ckpt/3999/assets/local/mecharm_redblock_box_teach_v1_trim2/norm_stats.json
        EXP=trim_corr_v3; N_MAC=24 ;;
    v4) NAME=mecharm_redblock_box_teach_v1_trim2_corr_v4b
        CONFIG=pi05_mecharm_trim_corr_v4_lora
        NORM_SRC=/workspace/ckpt/corr_v1_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v3/norm_stats.json
        EXP=trim_corr_v4; N_MAC=26 ;;
    v5) NAME=mecharm_redblock_box_teach_v1_trim2_corr_v5
        CONFIG=pi05_mecharm_trim_corr_v5_lora
        NORM_SRC=/workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json
        EXP=trim_corr_v5; N_MAC=30 ;;
    *) echo "[ERROR] 3 つ目は v3 か v4 か v5: $VARIANT" >&2; exit 2 ;;
esac
TAR=/workspace/data/${NAME}_raw_episodes.tar
DATA=/workspace/data/$NAME
OPENPI=/workspace/openpi
ADDON=/workspace/r2r2r/pi05/openpi_addon
NORM_SHA256=80346837eb12ffa6e683416f0fa41a196519fa8459d1f0b666b06cd621c7f61b   # Mac の写しの sha256 (v3・v4・v5 とも同じ trim2 の値)
NORM_DST=assets/$CONFIG/local/$NAME/norm_stats.json                           # $OPENPI からの相対

[ -f "$TAR" ] || { echo "[ERROR] tar がない: $TAR" >&2; exit 1; }
[ -f "$NORM_SRC" ] || { echo "[ERROR] trim2 の正規化の値がない: $NORM_SRC (3c で続きの元のモデルを取る)" >&2; exit 1; }
sha_of() { sha256sum "$1" | cut -d' ' -f1; }
[ "$(sha_of "$NORM_SRC")" = "$NORM_SHA256" ] \
    || { echo "[ERROR] $NORM_SRC が 3999 の学習の trim2 の値 (sha256 $NORM_SHA256) と違う" >&2; exit 1; }
if [ "$INIT" != base ] && [ ! -d "$INIT" ]; then
    echo "[ERROR] 続きから学習する params がない: $INIT" >&2; exit 1
fi
# shellcheck disable=SC1091
[ -f "$HOME/.local/bin/env" ] && source "$HOME/.local/bin/env"

echo "== 1. tar を広げる → $DATA"
# codex 必須: 前に広げたものへ上書きすると, tar を作り直して減ったエピソードやコマが残る.
# 空の一時フォルダに広げて確かめてから入れ替える (前のものは消す. 元は tar にある)
TMP="$DATA.tmp.$$"
rm -rf "$TMP"
mkdir -p "$TMP"
tar xf "$TAR" -C "$TMP"
N=$(find "$TMP/raw_episodes" -mindepth 2 -maxdepth 2 -name episode.npz | wc -l | tr -d ' ')
[ "$N" -gt 0 ] || { echo "[ERROR] tar にエピソードがない: $TAR" >&2; rm -rf "$TMP"; exit 1; }
rm -rf "$DATA"
mv "$TMP" "$DATA"
echo "   エピソード $N 本 (Mac で作ったときは $N_MAC 本)"

cd "$OPENPI"
echo "== 2. 学習設定 $CONFIG を足す (steps=$STEPS init=$INIT)"
python3 "$ADDON/add_trim_corr_config.py" "$OPENPI" --variant "$VARIANT" --steps "$STEPS" --init "$INIT"

echo "== 3. LeRobot 形式に変換 (local/$NAME)"
uv run python /workspace/r2r2r/pi05/convert_to_lerobot.py --raw-dir "$DATA/raw_episodes" --repo-id "local/$NAME"

echo "== 4. 正規化の値は trim2 の値をそのまま置く (計算し直さない)"
mkdir -p "$(dirname "$NORM_DST")"
cp "$NORM_SRC" "$NORM_DST"
[ "$(sha_of "$NORM_DST")" = "$NORM_SHA256" ] || { echo "[ERROR] 置いた値が違う: $NORM_DST" >&2; exit 1; }
echo "   $NORM_SRC → $OPENPI/$NORM_DST (sha256 ${NORM_SHA256:0:12}…)"

echo "== 5. 準備できました. 学習はこのコマンドで始める (このスクリプトでは流さない):"
echo "   cd $OPENPI && XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 uv run scripts/train.py $CONFIG --exp-name=$EXP --overwrite 2>&1 | tee /workspace/logs/train_$EXP.log"
```

### lab_lib.sh（commit 284dfd7）

#### lab_lib.sh

```bash
# lab_lib.sh — lab_start.sh / lab_stop.sh / lab_train.sh が読む共通の関数 (単体では動かさない)
#
# 鍵の扱い:
#   - Runpod の API キーは curl のヘッダーを <(printf ...) で渡す (printf は bash の組み込みなので
#     ps にもシェルの履歴にも出ない)
#   - Hugging Face の鍵は ssh の標準入力で Pod の ~/.cache/huggingface/token に書く
#   - Pod の IP とポートは画面・ログに出さない. Pod に入る ssh の出力は lab_mask で <POD_IP>・<SSH_PORT> に置き換える

LAB_ROOT=/Volumes/KIOXIA/r2r2r-data/lab
LAB_CURRENT="$LAB_ROOT/.lab_current"      # 今使っている Log 名 (lab_stop.sh が --log なしで読む)
RUNPOD_KEY_FILE=/Volumes/KIOXIA/keys/runpod_api_key.txt
HF_TOKEN_FILE=/Volumes/KIOXIA/keys/hf_token.txt
POD_SSH_KEY="$HOME/.ssh/id_ed25519"
RUNPOD_API=https://rest.runpod.io/v1
TUNNEL_PORT=8000
YEN_PER_USD=158
RUNPOD_HTTP=""             # runpod_api が最後に受けた HTTP の番号 ($(...) の中で呼ぶと親には残らない)

lab_need_val() {            # lab_need_val 例の値 "$@" (引数を読むループの中で) → 値がない・次が --… なら「--x に値がない (例 --x 例)」を出して終了コード 2 (依頼 55)
    local ex="$1"; shift
    if [ $# -lt 2 ] || [[ "$2" == --* ]]; then
        echo "[ERROR] $1 に値がない (例 $1 $ex)" >&2; exit 2
    fi
}

lab_need_key() {            # 鍵のファイルがない・空なら, どこに書けばよいかを出して止まる
    local f="$1" what="$2"
    if [ ! -s "$f" ]; then
        echo "[ERROR] $what がありません (または空です)." >&2
        echo "        次のファイルに鍵だけを1行で書いてください: $f" >&2
        exit 1
    fi
}

runpod_api() {              # runpod_api METHOD PATH [JSON]  → 本文を出す. 失敗の HTTP は戻り値 1
    local method="$1" path="$2" data="${3:-}" out code
    out=$(mktemp)
    if [ -n "$data" ]; then
        code=$(curl -sS -o "$out" -w '%{http_code}' -X "$method" \
            -H @<(printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' < "$RUNPOD_KEY_FILE")") \
            -H 'Content-Type: application/json' --data "$data" "$RUNPOD_API$path") || code=000
    else
        code=$(curl -sS -o "$out" -w '%{http_code}' -X "$method" \
            -H @<(printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' < "$RUNPOD_KEY_FILE")") \
            "$RUNPOD_API$path") || code=000
    fi
    cat "$out"
    rm -f "$out"
    RUNPOD_HTTP=$code
    case "$code" in 2??) return 0 ;; *) return 1 ;; esac
}

lab_pod_addr() {            # POD_ID から POD_IP / SSH_PORT を API で受け取る (画面には出さない)
    local j
    j=$(runpod_api GET "/pods/$POD_ID") || return 1
    POD_IP=$(jq -r '.publicIp // ""' <<<"$j")
    SSH_PORT=$(jq -r '.portMappings["22"] // ""' <<<"$j")
    POD_STATUS=$(jq -r '.desiredStatus // ""' <<<"$j")
    [ -n "$POD_IP" ] && [ -n "$SSH_PORT" ]
}

lab_mask() {                # Pod の IP とポートを置き換えて流す
    if [ -n "${POD_IP:-}" ] && [ -n "${SSH_PORT:-}" ]; then
        sed -l -e "s/${POD_IP//./\\.}/<POD_IP>/g" \
               -e "s/\([^0-9]\)${SSH_PORT}\([^0-9]\)/\1<SSH_PORT>\2/g" \
               -e "s/\([^0-9]\)${SSH_PORT}\$/\1<SSH_PORT>/"
    else
        cat
    fi
}

# known_hosts は使わない (Pod ごとに IP が変わる). LogLevel=ERROR で「Permanently added <IP>」も出さない
# ServerAliveCountMax=4: 返事のないつながりは 30 秒 × 4 で切る (つながったまま Pod 側が黙っているときは下の時間の上限で切る)
lab_ssh_opts() {
    SSH_OPTS=(-i "$POD_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
              -o LogLevel=ERROR -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=4)
}

# ---- Pod への ssh・scp の時間の上限 (依頼 52) ----
# 依頼 49 の初回で, 裏で起動したプロセスが ssh のつながりをつかんだまま ssh が 10 分以上返らなかった.
# pod_ssh・pod_scp は毎回この秒数で切る (切ったら終了コード 124). 長くかかるものは呼ぶ側で LAB_TO=秒 を前に付ける.
# トンネル (lab_start.sh 5. ssh -N -L) は推論の間ずっと使うので上限は付けない (ServerAlive で切れたつながりは閉じる)
LAB_TO_SHORT=60             # 様子を見る・印を書く・true など
LAB_SSH_TIMEOUT=600         # 既定 (LAB_TO がないとき)
LAB_TO_LONG=3600            # hf download・uv sync・tar やモデルの scp・Hugging Face へ上げる など

lab_timeout() {             # lab_timeout 秒 コマンド... → 秒を過ぎたら TERM (5 秒あとに KILL) して 124 (Mac には timeout がない)
    # perl の alarm で待つ (裏に sleep を残さない. $(...) の中でも, 残ったプロセスが出力をつかんで返らなくなることがない)
    # 切るときは子だけでなく孫以下 (scp が起動する ssh など) にも送る (codex R1. 残ると出力をつかんで返らない).
    # たどる間に新しい孫ができて取りこぼさないよう, 見つけたものから STOP で止め (止まったのを確かめてから) 子をたどり,
    # 増えなくなってから TERM と CONT を送る. 全部が終わるまで 5 秒待ち, 残りに KILL
    # (始まった時刻が見つけたときと同じものだけ. codex R2).
    # 別のプロセスグループには移さない (移すと ssh が端末の前のグループから外れ, Ctrl-C が届かず端末も読めなくなる).
    # 時間切れ・Ctrl-C (INT)・TERM・HUP は同じ1つの終わり方を通る. 入ったら alarm を止めてほかの合図は無視し,
    # 止めたものへ CONT を送り終えるまで途中で終わらない (codex R3). 終了コードは 124 / 130 / 143 / 129
    perl -MTime::HiRes=time,sleep -e '
        my $t = shift;
        my $pid = fork;
        defined $pid or die "fork: $!\n";
        if ($pid == 0) { exec { $ARGV[0] } @ARGV or exit 127; }
        my $reaped = 0;
        sub stat_of { my $s = `ps -o stat= -p $_[0] 2>/dev/null`; $s =~ s/\s+//g; return $s; }
        sub started { my $s = `ps -o lstart= -p $_[0] 2>/dev/null`; $s =~ s/\s+$//; return $s; }
        sub alive {         # まだある (ゾンビは終わったとみなす)
            my $p = shift;
            return 0 if $p == $pid && $reaped;
            my $st = stat_of($p);
            return $st ne "" && $st !~ /Z/;
        }
        sub stop_all {
            my %seen; my @q = ($pid);
            while (@q) {    # 止めながらたどる (止まったプロセスは子を作れない)
                my $p = shift @q;
                next if $seen{$p}++;
                kill "STOP", $p;
                my $until = time + 0.5;     # 止まった (T)・終わった のを確かめてから子を見る
                while (time < $until) { my $st = stat_of($p); last if $st eq "" || $st =~ /[TZ]/; sleep 0.01; }
                push @q, grep { /^\d+$/ } split /\s+/, `pgrep -P $p 2>/dev/null`;
            }
            my %t0 = map { $_ => started($_) } keys %seen;
            kill "TERM", keys %seen;
            kill "CONT", keys %seen;
            my $until = time + 5;           # 全部が終わるまで 5 秒まで待つ (経過時間で見る)
            while (time < $until) {
                $reaped = 1 if !$reaped && waitpid($pid, 1) > 0;
                last unless grep { alive($_) } keys %seen;
                sleep 0.1;
            }
            my @left = grep { alive($_) && $t0{$_} ne "" && started($_) eq $t0{$_} } keys %seen;
            kill "KILL", @left if @left;
            waitpid($pid, 0) unless $reaped;
        }
        sub finish {        # 時間切れ・合図で終わる道はここだけ (二度は入らない)
            my ($code, $msg) = @_;
            alarm 0;
            $SIG{$_} = "IGNORE" for qw(INT TERM HUP ALRM);
            stop_all();
            print STDERR $msg if $msg;
            exit $code;
        }
        $SIG{ALRM} = sub { finish(124, "[TIMEOUT] $ARGV[0] を $t 秒で切りました\n") };
        $SIG{INT}  = sub { finish(130) };
        $SIG{TERM} = sub { finish(143) };
        $SIG{HUP}  = sub { finish(129) };
        alarm $t;
        waitpid($pid, 0);
        exit(($? & 127) ? 128 + ($? & 127) : $? >> 8);' "$@"
}

pod_ssh() {                 # pod_ssh 'コマンド'  (標準入力はそのまま Pod へ. 時間の上限は LAB_TO か LAB_SSH_TIMEOUT 秒)
    { lab_timeout "${LAB_TO:-$LAB_SSH_TIMEOUT}" ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "root@$POD_IP" "$@" 2>&1; } | lab_mask
}

pod_scp() {                 # pod_scp 送る元... 送り先 (送り先の「:」の前に root@IP を付ける. 時間の上限は pod_ssh と同じ)
    { lab_timeout "${LAB_TO:-$LAB_SSH_TIMEOUT}" scp "${SSH_OPTS[@]}" -P "$SSH_PORT" "$@" 2>&1; } | lab_mask
}

lab_tunnel_pids() {         # このスクリプトが張った 8000 番のトンネル (ssh -N -L 8000:localhost:8000)
    pgrep -f "ssh -N -L ${TUNNEL_PORT}:localhost:${TUNNEL_PORT}" || true
}

# ---- lab_start.sh のつなぎ直し (依頼 44) ----

lab_valid_pods() {          # lab_valid_pods JSON → GET /pods の答えが「id を持つものの配列」なら 0 (読めない答えを「ない」にしない)
    jq -e 'type == "array" and all(.[]; type == "object" and (.id | type) == "string")' <<<"$1" >/dev/null 2>&1
}

lab_pod_kind() {            # lab_pod_kind ID → LAB_POD_KIND=running|stopped|gone と LAB_POD_STATUS (API の desiredStatus)
    # running = RUNNING. stopped = EXITED (止まっていてもディスクの料金がかかる). gone = TERMINATED,
    # または 404 で一覧にもない. それ以外 (つながらない・404 以外の失敗・答えが読めない・id が違う・知らない状態) は
    # 戻り値 1 (二重に借りないよう, 呼ぶ側で止まる)
    local id="$1" out j list rc
    LAB_POD_KIND=""; LAB_POD_STATUS=""
    out=$(mktemp)
    runpod_api GET "/pods/$id" > "$out"     # $(...) に入れない (RUNPOD_HTTP をこのシェルに残す)
    j=$(cat "$out"); rm -f "$out"
    case "$RUNPOD_HTTP" in
        2??) ;;
        404)    # 一覧にもないことを確かめてから「ない」とする
            list=$(runpod_api GET /pods) || return 1
            lab_valid_pods "$list" || return 1
            jq -e --arg id "$id" 'any(.[]; .id == $id and .desiredStatus != "TERMINATED")' <<<"$list" >/dev/null 2>&1
            rc=$?
            [ $rc = 1 ] && { LAB_POD_KIND=gone; return 0; }
            return 1 ;;
        *) return 1 ;;
    esac
    jq -e --arg id "$id" 'type == "object" and .id == $id' <<<"$j" >/dev/null 2>&1 || return 1
    LAB_POD_STATUS=$(jq -r '.desiredStatus // ""' <<<"$j")
    case "$LAB_POD_STATUS" in
        RUNNING) LAB_POD_KIND=running ;;
        EXITED) LAB_POD_KIND=stopped ;;
        TERMINATED) LAB_POD_KIND=gone ;;
        *) return 1 ;;
    esac
}

lab_probe_cmd() {           # lab_probe_cmd OPENPI_COMMIT モデルのフォルダ assets の norm_stats.json (フォルダからの相対) → Pod の中で流すコマンド (結果は PROBE の1行)
    # serve  = serve_policy.py のプロセスがある ([s] は, このコマンドを流す bash -c 自身に当たらないため)
    # listen = 8000 番 (16進 1F40) が LISTEN (0A). ss がないこともあるので /proc/net/tcp を見る
    # ckpt   = 3c の hf download が置く中身がある (依頼 47): _CHECKPOINT_METADATA・params/ の _METADATA と
    #          manifest.ocdbt・ocdbt.process_0/manifest.ocdbt・3f が写す norm_stats.json. 取りかけ
    #          (.cache/huggingface/download の *.incomplete) が残っている・探せないときは 0
    # mark   = hf download が最後まで済んだ印 (.lab_download_ok. 依頼 44 からの 3c が付ける. それより前に作った Pod にはない)
    # openpi = /workspace/openpi の HEAD が OPENPI_COMMIT
    # venv   = .venv の python と uv の env がある
    cat <<EOF
s=0; pgrep -f '[s]cripts/serve_policy\.py' >/dev/null && s=1
l=0; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk '\$4 == "0A" && \$2 ~ /:1F40\$/ {f=1} END {exit !f}' && l=1
c=0; [ -f $2/_CHECKPOINT_METADATA ] && [ -f $2/params/_METADATA ] && [ -f $2/params/manifest.ocdbt ] \\
  && [ -f $2/params/ocdbt.process_0/manifest.ocdbt ] && [ -f $2/$3 ] \\
  && { [ ! -d $2/.cache/huggingface ] || { r=\$(find $2/.cache/huggingface -name '*.incomplete' -print -quit 2>/dev/null) && [ -z "\$r" ]; }; } && c=1
m=0; [ -f $2/.lab_download_ok ] && m=1
o=0; h=\$(git -C /workspace/openpi rev-parse HEAD 2>/dev/null) && case "\$h" in $1*) o=1 ;; esac
v=0; [ -x /workspace/openpi/.venv/bin/python ] && [ -f \$HOME/.local/bin/env ] && v=1
echo "PROBE serve=\$s listen=\$l ckpt=\$c mark=\$m openpi=\$o venv=\$v"
EOF
}

lab_pod_probe() {           # lab_pod_probe OPENPI_COMMIT モデルのフォルダ norm_stats.json → PROBE_SERVE / PROBE_LISTEN / PROBE_CKPT / PROBE_MARK / PROBE_OPENPI / PROBE_VENV (1 か 0)
    local out line k
    out=$(pod_ssh "$(lab_probe_cmd "$1" "$2" "$3")") || return 1
    line=$(grep -E '^PROBE serve=[01] listen=[01] ckpt=[01] mark=[01] openpi=[01] venv=[01]$' <<<"$out" | tail -1)
    [ -n "$line" ] || return 1
    for k in serve listen ckpt mark openpi venv; do
        printf -v "PROBE_$(tr '[:lower:]' '[:upper:]' <<<"$k")" '%s' "$(sed -E "s/.* $k=([01]).*/\1/" <<<"$line")"
    done
}

lab_serve_stop_cmd() {      # Pod の中で serve_policy.py を止めるコマンド (30 秒まで終わるのを待ち, 残れば -9)
    # 起動のコマンド (scripts/serve_policy.py をそのまま含む) とは別の ssh で流す. 同じ bash -c に入れると
    # pkill がその bash -c 自身に当たる (依頼 47 の codex R1). このコマンドには [s] の形でしか書かない
    cat <<'EOF'
tmux kill-session -t serve 2>/dev/null; pkill -f '[s]cripts/serve_policy\.py'
for i in $(seq 1 30); do pgrep -f '[s]cripts/serve_policy\.py' >/dev/null || break; sleep 1; done
pkill -9 -f '[s]cripts/serve_policy\.py'; sleep 1; ! pgrep -f '[s]cripts/serve_policy\.py' >/dev/null
EOF
}

lab_archive_state() {       # lab_archive_state FILE → 消さずに <名前>_<時刻>.json に変えて残す. 新しい名前は LAB_ARCHIVED
    local f="$1" dest
    dest="${f%.json}_$(date +%Y%m%d_%H%M%S).json"
    [ -e "$dest" ] && dest="${f%.json}_$(date +%Y%m%d_%H%M%S)_$$.json"
    mv "$f" "$dest" && LAB_ARCHIVED="$dest"
}

# ---- Pod の準備で流すコマンド (lab_start.sh 3b〜3f と lab_train.sh が同じものを流す. 依頼 49 で lab_start.sh から移した) ----
LAB_OPENPI_COMMIT=215abfb
LAB_TEMPLATE_ID=runpod-torch-v280
LAB_IMAGE=runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404

lab_cmd_hf_install() { echo "pip install -q -U --break-system-packages huggingface_hub"; }

lab_send_hf_token() {       # Hugging Face の鍵を ssh の標準入力で渡す (コマンドの引数・履歴に残らない). hf はこのファイルを読む
    tr -d '\r\n' < "$HF_TOKEN_FILE" | pod_ssh 'umask 077; mkdir -p ~/.cache/huggingface; cat > ~/.cache/huggingface/token'
}

lab_cmd_ckpt() {            # lab_cmd_ckpt リポジトリ 置く場所 → hf download (最後まで済んだら .lab_download_ok を付ける)
    echo "rm -f $2/.lab_download_ok && hf download $1 --local-dir $2 && touch $2/.lab_download_ok && du -sh $2"
}

lab_cmd_openpi() {          # lab_cmd_openpi コミット
    echo "cd /workspace && { [ -d openpi/.git ] || git clone -q --recurse-submodules https://github.com/Physical-Intelligence/openpi.git; } && cd openpi && git checkout -q $1 && git log --oneline -1"
}

lab_cmd_uv_sync() {
    echo "curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null && source \$HOME/.local/bin/env && cd /workspace/openpi && GIT_LFS_SKIP_SMUDGE=1 uv sync"
}

lab_cmd_addon() {           # 3f. 何度流しても同じ結果 (install_addon.py・add_trim_config.py とも二重に入れない)
    echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi && mkdir -p assets/pi05_mecharm_trim_lora/local && cp -r /workspace/ckpt/3999/assets/local/mecharm_redblock_box_teach_v1_trim2 assets/pi05_mecharm_trim_lora/local/"
}

# ---- lab_start.sh で推論に使うモデル (依頼 52. --model trim | corr. 依頼 57 で corr2) ----
# corr の学習の設定 (pi05_mecharm_trim_corr_lora) は lab_train.sh の学習と同じ値で足す (add_trim_corr_config.py は
# 入っている設定と --steps / --init が違うと止まる. 推論では weight_loader は使わない)
# corr2 (依頼 57) は corr_v1_1499 から trim2_corr_v4 で続きを学習したもの (設定 pi05_mecharm_trim_corr_v4_lora)
# corr3 (依頼 97) は corr_v2_1499 から trim2_corr_v5 で続きを学習したもの (設定 pi05_mecharm_trim_corr_v5_lora)
LAB_MODELS="trim corr corr2 corr3"
LAB_CORR_STEPS=1500
LAB_CORR_INIT=/workspace/ckpt/3999/params
LAB_CORR2_STEPS=1500
LAB_CORR2_INIT=/workspace/ckpt/corr_v1_1499/params
LAB_CORR3_STEPS=1500
LAB_CORR3_INIT=/workspace/ckpt/corr_v2_1499/params
lab_model_def() {           # lab_model_def trim|corr → MODEL_REPO MODEL_CKPT MODEL_CONFIG MODEL_DATA MODEL_DESC. 知らない名前なら 1
    case "$1" in
        trim)
            MODEL_REPO=PixelstoMotors/pi05-mecharm-trim-lora-v1
            MODEL_CKPT=/workspace/ckpt/3999
            MODEL_CONFIG=pi05_mecharm_trim_lora
            MODEL_DATA=mecharm_redblock_box_teach_v1_trim2
            MODEL_DESC="trim2 で学習した 3999 (今までのモデル)" ;;
        corr)
            MODEL_REPO=PixelstoMotors/pi05-mecharm-trim-corr-lora-v1
            MODEL_CKPT=/workspace/ckpt/corr_v1_1499
            MODEL_CONFIG=pi05_mecharm_trim_corr_lora
            MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v3
            MODEL_DESC="3999 から trim2_corr_v3 で続きを学習した 1499 (依頼 49)" ;;
        corr2)
            MODEL_REPO=PixelstoMotors/pi05-mecharm-trim-corr-lora-v2
            MODEL_CKPT=/workspace/ckpt/corr_v2_1499
            MODEL_CONFIG=pi05_mecharm_trim_corr_v4_lora
            MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v4b     # 依頼 58: 182545 を外した 7 本 (8 本の corr_v4 は使わない)
            MODEL_DESC="corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (依頼 57 / 58)" ;;
        corr3)
            MODEL_REPO=PixelstoMotors/pi05-mecharm-trim-corr-lora-v3
            MODEL_CKPT=/workspace/ckpt/corr_v3_1499
            MODEL_CONFIG=pi05_mecharm_trim_corr_v5_lora
            MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v5      # 依頼 97: corr_v4b の 7 本 + 11-06 のゲームパッドの 4 本
            MODEL_DESC="corr2 の 1499 から trim2_corr_v5 で続きを学習した 1499 (依頼 97)" ;;
        *) return 1 ;;
    esac
}

lab_cmd_addon_model() {     # lab_cmd_addon_model trim|corr|corr2|corr3 → 3f (trim は lab_cmd_addon と同じ. corr〜corr3 は学習の設定と assets も足す)
    lab_model_def "$1" || return 1
    if [ "$1" = trim ]; then lab_cmd_addon; return; fi
    local corr_args="--steps $LAB_CORR_STEPS --init $LAB_CORR_INIT"
    [ "$1" = corr2 ] && corr_args="--variant v4 --steps $LAB_CORR2_STEPS --init $LAB_CORR2_INIT"
    [ "$1" = corr3 ] && corr_args="--variant v5 --steps $LAB_CORR3_STEPS --init $LAB_CORR3_INIT"
    # assets は hf download が置いたモデルの assets/local/<データ名> を, openpi の assets/<設定>/local/ に写す
    # (trim の 3f が assets/pi05_mecharm_trim_lora/local/ に写すのと同じ形. 中身は trim2 と同じ正規化の値)
    echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/add_trim_corr_config.py /workspace/openpi $corr_args && mkdir -p assets/$MODEL_CONFIG/local && cp -r $MODEL_CKPT/assets/local/$MODEL_DATA assets/$MODEL_CONFIG/local/"
}

# ---- lab_train.sh で作るモデル (依頼 57. --model corr | corr2. 依頼 97 で corr3) ----
# 作るモデルの名前は lab_start.sh の --model と同じ. 続きの元 (TRAIN_FROM) も lab_model_def のモデル
LAB_TRAIN_MODELS="corr corr2 corr3"
lab_train_def() {           # lab_train_def corr|corr2|corr3 → TRAIN_* (知らない名前なら 1)
    case "$1" in
        corr)                   # 依頼 49: 3999 から trim2_corr_v3 で続きを学習した 1499
            TRAIN_FROM=trim; TRAIN_VARIANT=v3
            TRAIN_FROM_KIOXIA=pi05_mecharm_trim_lora_v1/3999 ;;       # checkpoints/ からの相対 (由来の指紋を取る. 依頼 54)
        corr2)                  # 依頼 57: corr の 1499 から trim2_corr_v4b (依頼 58) で続きを学習する
            TRAIN_FROM=corr; TRAIN_VARIANT=v4
            TRAIN_FROM_KIOXIA=pi05_mecharm_trim_corr_lora_v1/1499 ;;
        corr3)                  # 依頼 97: corr2 の 1499 から trim2_corr_v5 で続きを学習する
            TRAIN_FROM=corr2; TRAIN_VARIANT=v5
            TRAIN_FROM_KIOXIA=pi05_mecharm_trim_corr_lora_v2/1499 ;;
        *) return 1 ;;
    esac
    lab_model_def "$1" || return 1
    TRAIN_DATA=$MODEL_DATA; TRAIN_CONFIG=$MODEL_CONFIG; TRAIN_REPO_OUT=$MODEL_REPO
    TRAIN_CKPT_NAME=${MODEL_REPO#*/pi05-}; TRAIN_CKPT_NAME=pi05_${TRAIN_CKPT_NAME//-/_}   # 例 pi05_mecharm_trim_corr_lora_v2
    TRAIN_EXP=trim_corr_${TRAIN_VARIANT}
    lab_model_def "$TRAIN_FROM" || return 1
    TRAIN_REPO_IN=$MODEL_REPO; TRAIN_CKPT_IN=$MODEL_CKPT; TRAIN_INIT=$MODEL_CKPT/params
    lab_model_def "$1"          # MODEL_* は作るモデルのものに戻す
}

lab_cmd_addon_train() {     # lab_cmd_addon_train corr|corr2|corr3 → lab_train.sh の 3f
    # corr は lab_cmd_addon と同じ (3999 を取っているので trim の assets も写す). corr2・corr3 は 3999 を取らないので
    # addon と trim2 の設定だけを入れる (学習の設定と正規化の値は pod_prep_trim_corr.sh が置く)
    if [ "$1" = corr ]; then lab_cmd_addon; return; fi
    echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi"
}

lab_serve_dir_cmd() {       # Pod の中で, 動いている serve_policy.py の --policy.dir を出す (SERVE_DIR の1行. 動いていなければ何も出さない)
    # 当たったプロセス (tmux・sh -c・uv run・python) ごとに引数を読み, どれか1つでも --policy.dir が読めない・
    # 2つ以上ある・使えない文字を含む・ほかと違う, なら「?」(codex R1: 読めたものだけをまとめない. 文字を削って直さない).
    # 「--policy.dir=X」と「--policy.dir X」の両方を読む. [s] の形は, このコマンドを流す bash -c 自身に当たらないため
    cat <<'EOF'
pids=$(pgrep -f '[s]cripts/serve_policy\.py')
if [ -n "$pids" ]; then
  sd=; bad=0
  for p in $pids; do
    a=$(ps -o args= -p "$p" 2>/dev/null) || continue      # 調べる間に終わったものは数えない
    [ -n "$a" ] || continue
    # --policy.dir がちょうど 1 つのときだけ値を見る (「--policy.dir=X --policy.dir=」のように空の値があっても数える. codex R2)
    [ "$(printf '%s\n' "$a" | grep -oE -- '--policy\.dir([= ]|$)' | wc -l | tr -d ' ')" = 1 ] || { bad=1; break; }
    v=$(printf '%s\n' "$a" | grep -oE -- '--policy\.dir(=| +)[^ ]*' | sed -E 's/^--policy\.dir(=| +)//')
    printf '%s\n' "$v" | grep -qE '^/[A-Za-z0-9_./-]+$' || { bad=1; break; }
    if [ -z "$sd" ]; then sd=$v; elif [ "$sd" != "$v" ]; then bad=1; break; fi
  done
  [ $bad = 0 ] && [ -n "$sd" ] || sd='?'
  echo "SERVE_DIR $sd"
fi
EOF
}

lab_pod_serve_dir() {       # → SERVE_DIR (動いているサーバーの --policy.dir. 動いていなければ空. 読めなければ ?). ssh ができなければ 1
    local out
    out=$(LAB_TO=$LAB_TO_SHORT pod_ssh "$(lab_serve_dir_cmd)") || return 1
    SERVE_DIR=$(grep -E '^SERVE_DIR ' <<<"$out" | tail -1 | cut -d' ' -f2-)
    return 0
}

lab_model_of_dir() {        # lab_model_of_dir /workspace/ckpt/... → そのフォルダを使うモデルの名前 (なければ空)
    local m
    for m in $LAB_MODELS; do
        lab_model_def "$m" && [ "$MODEL_CKPT" = "$1" ] && { echo "$m"; return 0; }
    done
    return 1
}

lab_cost_line() {           # lab_cost_line 秒 1時間のドル → 「借りていた時間 × 料金 = ドル (円)」の1行
    awk -v s="$1" -v c="$2" -v y="$YEN_PER_USD" 'BEGIN {
        h = s / 3600; usd = h * c
        printf "借りていた時間 %d 分 %d 秒 (%.2f 時間) × 1時間 %.2f ドル = 約 %.2f ドル (約 %d 円. 1ドル=%d円)\n",
               int(s / 60), s % 60, h, c, usd, int(usd * y + 0.5), y }'
}

# ---- lab_train.sh (依頼 49) が Pod の中で流すコマンド. テストでこの Mac の bash でも流すので, GNU と BSD の両方で動く書き方にする ----
# pgrep / pkill -f は, そのコマンドを流している bash -c 自身の引数にも当たる (依頼 47 の codex R1).
# 裏で回すものは「bash <パス>」「nvidia-smi --query-gpu=timestamp」で始まる形で起動し, ^ を付けた形で探す
# (bash -c の引数は「bash -c」で始まるので当たらない)
LAB_RUN_PAT='^bash /workspace/r2r2r/train_run\.sh$'
LAB_WD_PAT='^bash /workspace/r2r2r/lab_watchdog\.sh$'
LAB_GPU_PAT='^nvidia-smi --query-gpu=timestamp'

lab_train_start_cmd() {     # lab_train_start_cmd 設定 exp 学習のログ GPU のログ 回したログ 終わりの印 → 学習を setsid nohup で始める (STARTED が出れば始まった)
    # PYTHONUNBUFFERED=1 (依頼 52): 「Step N: ... loss=」は標準出力 (tqdm.write) に出る. ファイルに向けるとためこまれ,
    # 学習の最後にまとめて書かれていた (依頼 49 の初回)
    cat <<EOF
: lt=start_train
rm -f $6
cat > /workspace/r2r2r/train_run.sh <<'RUN_EOF'
#!/bin/bash
# lab_train.sh が置く. train.py を回し, 終わったら終了コードを $6 に書く
echo "\$(date '+%F %T') 始める" >> $5
{ cd /workspace/openpi && source \$HOME/.local/bin/env \\
  && PYTHONUNBUFFERED=1 XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 uv run scripts/train.py $1 --exp-name=$2 --overwrite > $3 2>&1; }
rc=\$?
echo "\$(date '+%F %T') 終わった (終了コード \$rc)" >> $5
echo \$rc > $6
RUN_EOF
setsid nohup bash /workspace/r2r2r/train_run.sh >/dev/null 2>&1 < /dev/null &
pkill -f '$LAB_GPU_PAT'
setsid nohup nvidia-smi --query-gpu=timestamp,name,memory.used,memory.total,utilization.gpu,temperature.gpu,power.draw --format=csv -l 30 > $4 2>&1 < /dev/null &
sleep 3
pgrep -f '$LAB_RUN_PAT' >/dev/null && echo STARTED
EOF
}

lab_train_kill_cmd() {      # 学習を止める (片付けの前. 書き終わったログを取るため)
    echo ": lt=kill_train; pkill -f '$LAB_RUN_PAT'; pkill -f '[s]cripts/train\.py'; sleep 3; pkill -9 -f '[s]cripts/train\.py'; true"
}

lab_train_status_cmd() {    # lab_train_status_cmd 学習のログ 終わりの印 チェックポイントのフォルダ ステップ数 → STATUS の1行
    # run  = 学習を回すスクリプト (train_run.sh) が動いている. train.py が終わったあと終わりの印を書いてから終わるので,
    #        run=0 で exit=- なら「書く前に消えた」(殺された)
    # exit = train.py の終了コード (まだなら -)
    # age  = 学習のログが最後に書かれてからの秒 (tqdm が 1 ステップごとに書くので, 長く書かれなければ止まっている)
    # bar  = tqdm の「N/ステップ数」の N (または openpi の「Progress on: N/…」. 3 けたに丸めてある).
    #        logstep / loss = 最後の「Step N: ... loss=...」の行
    # ckpt = 数字だけのフォルダのいちばん大きい番号 (orbax の書きかけ *.orbax-checkpoint-tmp-* は数えない)
    # cm   = そのフォルダに _CHECKPOINT_METADATA がある
    cat <<EOF
: lt=status
L=$1; E=$2; C=$3
r=0; pgrep -f "$LAB_RUN_PAT" >/dev/null && r=1
e=-; [ -f \$E ] && e=\$(tr -d ' \n' < \$E)
a=-; s=-; ls=-; lo=-
if [ -f \$L ]; then
  mt=\$(stat -c %Y \$L 2>/dev/null || date -r \$L +%s 2>/dev/null)     # GNU (Pod) は stat -c, Mac (テスト) は date -r
  case "\$mt" in ''|*[!0-9]*) a=- ;; *) a=\$(( \$(date +%s) - mt )) ;; esac
  t=\$(tail -c 400000 \$L | tr '\r' '\n')
  s=\$(printf '%s\n' "\$t" | grep -oE '[0-9]+/$4 \[' | tail -1 | cut -d/ -f1)
  # openpi の tqdm (tqdm_loggable) はファイルに向けると「Progress on: 1.23kit/1.50kit ...」の形で書く (依頼 52).
  # 数は 3 けたに丸めてある (k = 1000). 「-/1500」(まだ始まっていない) は数えない
  [ -n "\$s" ] || s=\$(printf '%s\n' "\$t" | grep -oE 'Progress on: [0-9.]+[kM]?it/' | tail -1 \\
      | awk '{ v = \$3; sub(/it\/\$/, "", v); m = 1; if (v ~ /k\$/) { m = 1000; sub(/k\$/, "", v) } else if (v ~ /M\$/) { m = 1000000; sub(/M\$/, "", v) }; printf "%d", v * m + 0.5 }')
  ln=\$(printf '%s\n' "\$t" | grep -E '^Step [0-9]+:' | tail -1)
  [ -n "\$ln" ] && { ls=\$(printf '%s\n' "\$ln" | sed -E 's/^Step ([0-9]+):.*/\1/'); lo=\$(printf '%s\n' "\$ln" | grep -oE '(^|[ ,])loss=[0-9.eE+-]+' | head -1 | sed 's/.*=//'); }
fi
c=\$(ls \$C 2>/dev/null | grep -E '^[0-9]+\$' | sort -n | tail -1)
m=0; [ -n "\$c" ] && [ -f \$C/\$c/_CHECKPOINT_METADATA ] && m=1
echo "STATUS run=\$r exit=\${e:--} age=\${a:--} bar=\${s:--} logstep=\${ls:--} loss=\${lo:--} ckpt=\${c:--} cm=\$m"
EOF
}

lab_train_parse_status() {  # lab_train_parse_status 出力 → S_RUN S_EXIT S_AGE S_BAR S_LOGSTEP S_LOSS S_CKPT S_CM. STATUS の行がなければ 1
    local line k v
    line=$(grep -E '^STATUS run=[01] ' <<<"$1" | tail -1)
    [ -n "$line" ] || return 1
    for k in run exit age bar logstep loss ckpt cm; do
        v=$(sed -nE "s/.* $k=([^ ]*).*/\1/p" <<<" ${line#STATUS }")
        printf -v "S_$(tr '[:lower:]' '[:upper:]' <<<"$k")" '%s' "${v:--}"
    done
}

lab_train_list_cmd() {      # lab_train_list_cmd フォルダ → 「F<TAB>パス<TAB>バイト数」を名前順に (train_state/ は数えない)
    echo ": lt=list_ckpt; set -o pipefail; cd $1 && find . -type f ! -path './train_state/*' -printf '%P\t%s\n' | LC_ALL=C sort | sed 's/^/F\t/'"
}

lab_list_files() {          # lab_list_files フォルダ → 「パス<TAB>バイト数」を名前順に (Mac 側. du は KIOXIA でずれるので stat の大きさ)
    # 数えない: .cache/ (hf download の印)・._* (exFAT が付ける)・.DS_Store・.gitattributes・train_state/
    python3 -I -c '
import os, sys
root = sys.argv[1]
out = []
def err(e):
    raise e                 # 読めないフォルダを黙って飛ばさない (一覧が足りないまま「合った」としない)
for d, dirs, files in os.walk(root, onerror=err):
    rel = os.path.relpath(d, root)
    if rel == ".":
        dirs[:] = [x for x in dirs if x not in (".cache", "train_state")]
    for f in files:
        if f.startswith("._") or f in (".DS_Store", ".gitattributes"):
            continue
        p = os.path.normpath(os.path.join(rel, f))
        out.append((p, os.path.getsize(os.path.join(d, f))))
for p, s in sorted(out, key=lambda x: x[0].encode()):
    print(f"{p}\t{s}")
' "$1"
}

lab_watchdog_script() {     # Pod 側の上限の見張り (lab_train.sh が /workspace/r2r2r/lab_watchdog.sh に置き, setsid nohup で裏で回す)
    cat <<'WATCHDOG_EOF'
#!/bin/bash
# lab_watchdog.sh — 上限の時刻を過ぎたら, この Pod を自分で消す (lab_train.sh が置く. 依頼 49)
# Mac が止まって lab_train.sh が Pod を消せなくても, Pod が残り続けないための保険.
# 鍵は Mac から送らない. Runpod が Pod に入れる RUNPOD_POD_ID と Pod 用の鍵 RUNPOD_API_KEY を使う
# (ssh のシェルには入っていないことがあるので, 1 番のプロセスの環境と /etc/rp_environment からも探す). 鍵は書き出さない
F=${LAB_WD_DEADLINE:-/workspace/logs/.lab_deadline}     # 上限の時刻 (epoch). lab_train.sh が書き換えて延ばす
L=${LAB_WD_LOG:-/workspace/logs/watchdog.log}
RUNPOD_REST=${LAB_WD_API:-https://rest.runpod.io/v1}
penv() {
    { tr '\0' '\n' < /proc/1/environ; sed -E 's/^export //; s/"//g' /etc/rp_environment; } 2>/dev/null \
        | sed -n "s/^$1=//p" | head -1
}
while :; do
    d=$(cat "$F" 2>/dev/null)
    case "$d" in ''|*[!0-9]*) ;; *) [ "$(date +%s)" -ge "$d" ] && break ;; esac
    sleep "${LAB_WD_POLL:-60}"
done
echo "$(date '+%F %T') 上限の時刻を過ぎた. この Pod を消します" >> "$L"
id=${RUNPOD_POD_ID:-$(penv RUNPOD_POD_ID)}
key=${RUNPOD_API_KEY:-$(penv RUNPOD_API_KEY)}
[ -n "$id" ] || { echo "RUNPOD_POD_ID が見つからない. 消せない" >> "$L"; exit 1; }
# 通信が一時だめでも残らないよう, DELETE と runpodctl remove を間を置いてくり返す (runpodctl の出力は鍵を含まないが, 念のため捨てる)
for i in $(seq 1 "${LAB_WD_TRIES:-30}"); do
    code=$(curl -sS --max-time 30 -o /dev/null -w '%{http_code}' -X DELETE \
        -H @<(printf 'Authorization: Bearer %s\n' "$key") "$RUNPOD_REST/pods/$id" 2>/dev/null) || code=000
    echo "$(date '+%F %T') ($i) DELETE → HTTP $code" >> "$L"
    case "$code" in 2??) exit 0 ;; esac
    if command -v runpodctl >/dev/null 2>&1 && RUNPOD_API_KEY="$key" timeout "${LAB_WD_CTL_TIMEOUT:-60}" runpodctl remove pod "$id" >/dev/null 2>&1; then
        echo "$(date '+%F %T') ($i) runpodctl remove pod で消した" >> "$L"; exit 0
    fi
    sleep "${LAB_WD_RETRY:-60}"
done
# 消せなければ, せめて止める (GPU の料金は止まる. ディスクの料金は残る)
if command -v runpodctl >/dev/null 2>&1 && RUNPOD_API_KEY="$key" timeout "${LAB_WD_CTL_TIMEOUT:-60}" runpodctl stop pod "$id" >/dev/null 2>&1; then
    echo "$(date '+%F %T') 消せず, runpodctl stop pod で止めた (ディスクの料金は残る)" >> "$L"; exit 0
fi
echo "$(date '+%F %T') 消せなかった" >> "$L"
exit 1
WATCHDOG_EOF
}

lab_watchdog_start_cmd() {  # lab_watchdog_start_cmd 上限の時刻 → 見張りを (なければ) 起動し, 様子を WATCHDOG の1行で出す (鍵の中身は出さない)
    # 依頼 52: 前は「pgrep ... || setsid nohup ... &」だった. & が「a || b」の全体に付くので, 裏の子シェルが
    # ssh の出力をつかんだまま見張りの終わりを待ち, ssh が返らなかった. if の中で setsid nohup だけを裏に回す
    cat <<EOF
: lt=watchdog
mkdir -p /workspace/logs; echo $1 > /workspace/logs/.lab_deadline.tmp && mv /workspace/logs/.lab_deadline.tmp /workspace/logs/.lab_deadline; chmod +x /workspace/r2r2r/lab_watchdog.sh
if ! pgrep -f '$LAB_WD_PAT' >/dev/null; then setsid nohup bash /workspace/r2r2r/lab_watchdog.sh >/dev/null 2>&1 < /dev/null & fi
sleep 1
pe() { [ -n "\$(printenv \$1)" ] || { tr '\0' '\n' < /proc/1/environ; cat /etc/rp_environment; } 2>/dev/null | grep -qE "^(export )?\$1=.+"; }
w=0; pgrep -f '$LAB_WD_PAT' >/dev/null && w=1
p=0; pe RUNPOD_POD_ID && p=1
k=0; pe RUNPOD_API_KEY && k=1
c=0; command -v runpodctl >/dev/null 2>&1 && c=1
echo "WATCHDOG run=\$w pod=\$p key=\$k ctl=\$c"
EOF
}

# ---- lab_train.sh の Mac 側 (--dry-run とテストでは lab_train_fakes.sh が差し替える) ----

lab_have_hf_cli() { command -v hf >/dev/null 2>&1; }

lab_hf_download_mac() {     # lab_hf_download_mac リポジトリ 置く場所 → Mac の hf で取る. 鍵は HF_TOKEN_PATH でファイルの場所だけを渡す
    env -u HF_TOKEN HF_TOKEN_PATH="$HF_TOKEN_FILE" HF_HUB_DISABLE_TELEMETRY=1 hf download "$1" --local-dir "$2" > "$2.download.log" 2>&1
    local rc=$?
    tail -3 "$2.download.log"; rm -f "$2.download.log"
    return $rc
}

lab_hf_upload_file_mac() {  # lab_hf_upload_file_mac リポジトリ ファイル リポジトリの中の名前 コミットの文 → Mac の hf で1つ上げる (依頼 54)
    # リポジトリは作らない (hf_upload_ckpt.py が非公開で作ったものにだけ足す). 鍵は HF_TOKEN_PATH でファイルの場所だけを渡す
    local log rc
    log=$(mktemp)
    ( unset HF_TOKEN; export HF_TOKEN_PATH="$HF_TOKEN_FILE" HF_HUB_DISABLE_TELEMETRY=1 HF_HUB_DISABLE_PROGRESS_BARS=1
      lab_timeout 120 hf upload "$1" "$2" "$3" --repo-type model --private --commit-message "$4" ) > "$log" 2>&1
    rc=$?
    tail -2 "$log"; rm -f "$log"
    return $rc
}

lab_notify() {              # lab_notify 題 文 終了コード → Mac の通知 (0 なら Glass, それ以外は Basso の音)
    local sound=Basso
    [ "$3" = 0 ] && sound=Glass
    osascript -e 'on run argv' -e 'display notification (item 2 of argv) with title (item 1 of argv) sound name (item 3 of argv)' \
              -e 'end run' "$1" "$2" "$sound" >/dev/null 2>&1 \
        || echo "[WARN  ] Mac の通知を出せなかった"
}

lab_keep_awake() {          # このスクリプトが終わるまで Mac をスリープさせない (CAFF_PID)
    caffeinate -i -s -w $$ >/dev/null 2>&1 &
    CAFF_PID=$!
    echo "[MAC   ] 終わるまでスリープしないようにした (caffeinate pid $CAFF_PID). ふたは閉じない"
    if ! pmset -g batt 2>/dev/null | grep -q "'AC Power'"; then
        echo "[WARN  ] 電源につながっていない. バッテリーだとスリープを止めきれないので, 電源につないでください"
    fi
}
```

### lab_start.sh（commit 284dfd7）

#### lab_start.sh

```bash
#!/bin/bash
# lab_start.sh — Runpod の Pod の準備から mechArm 270 の実機推論までを1行で行う (依頼 37 / 手順は LOG407「起動の手順」)
#
#   ./lab_start.sh --gpu A40 --episode run_0001_20261019_194818 --log Log17
#   ./lab_start.sh --log Log17 --dry-run      アームなしで試す (デーモン・HOME・爪・位置合わせを飛ばし,
#                                             run_policy_client.py を --execute なし・--dry-keys で呼ぶ)
#
#   --gpu      GPU の種類 (既定 A40). 「A40」は「NVIDIA A40」として API に渡す. 名前に空白を含む種類は
#              「--gpu "NVIDIA RTX A6000"」のように Runpod の名前のまま渡す
#   --episode  位置合わせ (camera_align.py) で重ねるお手本 (既定 run_0001_20261019_194818)
#   --log      ターミナルの出力を残す /Volumes/KIOXIA/r2r2r-data/lab/<Log>/ (なければ聞く)
#   --model    推論に使うモデル (依頼 52). trim (既定) = trim2 で学習した 3999 (PixelstoMotors/pi05-mecharm-trim-lora-v1)
#              corr = 3999 から trim2_corr_v3 で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v1)
#              corr2 = corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2.
#                      依頼 57. lab_train.sh --model corr2 で作る. Pod の /workspace/ckpt/corr_v2_1499)
#              corr3 = corr2 の 1499 から trim2_corr_v5 で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3.
#                      依頼 97. lab_train.sh --model corr3 で作る. Pod の /workspace/ckpt/corr_v3_1499)
#              取る場所・設定・assets・サーバーの起動・つなぎ直しの確かめが選んだモデルに合う. 安全範囲 (学習データ ±10°)
#              と位置合わせのお手本はどれも trim2 の raw_episodes (corr_v3・corr_v4 と合わせても範囲はほぼ同じで, trim2 の方が狭い.
#              corr_v5 は 11-06 の 184650 の j2 が trim2 の上限 +31.4° を最大 11° 超える)
#              つなぎ直しで, 残っている Pod のサーバーが別のモデルで動いていれば, そのことを出して止まる
#
# 1. Pod を作る (A40×1・Runpod Pytorch 2.8.0・コンテナのディスク 200GB・22/tcp を外へ出す)
#    「resources」で借りられなければ 30 秒待って, 合わせて 5 回まで作り直す
#    <Log>/pod/lab_pod.json の Pod が API で RUNNING なら, 作らずにつなぎ直す (依頼 44)
#      - 3. で Pod の中を見る. サーバーが 8000 番で待っていれば 3b〜4 を飛ばす. 落ちていれば前の serve.log を
#        持ち帰り, モデル・openpi・.venv のないものだけやり直し, 3f と 4 を流す. 3a (スクリプトを送る) は毎回
#        プロセスはあるのに 8000 番で待っていなければ 30 秒待ち, それでも待っていなければ起動し直す (依頼 47)
#      - 止まっている (EXITED など) なら lab_stop.sh --skip-log で消してから作る. API にない・TERMINATED なら作る
#        どちらも古い lab_pod.json は lab_pod_<時刻>.json に名前を変えて残す
#      - API で様子が分からないとき・記録にない p2m-<Log> の Pod が残っているときは, 二重に借りないよう止まる
# 2. Pod が立ち上がり, SSH の IP とポートが API から出るまで待つ (画面・ログには出さない)
# 3. Pod を準備する (3b〜3f のコマンドは lab_lib.sh. lab_train.sh と同じもの) (pi05/ と add_trim_config.py → huggingface_hub → hf download → openpi 215abfb → uv sync
#    → addon・設定・assets → serve_policy.py を tmux の中で起動し /workspace/logs/serve.log に残す)
# 4. serve.log に「server listening on 0.0.0.0:8000」が出るまで待つ
# 5. Mac の 8000 番へのトンネルを裏で張る (このスクリプトが終わるときに閉じる)
# 6. tools/daemon_ctl.sh status で 5555 の LISTEN を見る. なければ start
# 7. HOME → 爪を開く → camera_align.py (q で閉じる)
# 8. run_policy_client.py (LOG407 と同じ引数). 終わったら, もう1回やるかを聞く (y なら 7 へ)
# 9. Pod は消さない. 片付けは ./lab_stop.sh
#
# Pod の情報 (id・GPU・1時間の料金・作った時刻・サーバーを起動したモデル) は <Log>/pod/lab_pod.json に残す (IP とポートは残さない).

set -u
set -o pipefail

# 中身は全部 main の中に置き, 最後の1行で呼ぶ (依頼 47). bash はスクリプトを少しずつ読みながら流すので,
# 動いている途中でこのファイルが書き換わると, 続きを新しい中身の途中から読んで壊れる. 関数は呼ぶ前に最後まで読む
main() {
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=lab_lib.sh
. "$HERE/lab_lib.sh"

ORIG_ARGS=("$@")
GPU=A40
EPISODE=run_0001_20261019_194818
LOG=""
DRY_RUN=0
MODEL=trim
while [ $# -gt 0 ]; do
    case "$1" in
        --gpu) lab_need_val A40 "$@"; GPU="$2"; shift 2 ;;
        --episode) lab_need_val run_0001_20261019_194818 "$@"; EPISODE="$2"; shift 2 ;;
        --log) lab_need_val Log18 "$@"; LOG="$2"; shift 2 ;;
        --dry-run) DRY_RUN=1; shift ;;
        --model) [ $# -ge 2 ] || { echo "[ERROR] --model には ${LAB_MODELS// / か } を付ける" >&2; exit 2; }     # 値がないと shift 2 が失敗して回り続ける (codex R1)
                 MODEL="$2"; shift 2 ;;
        -h|--help) sed -n '2,42p' "$0"; exit 0 ;;
        *) echo "[ERROR] 知らない引数: $1  (./lab_start.sh --help)" >&2; exit 2 ;;
    esac
done
case "$GPU" in *" "*) GPU_ID="$GPU" ;; *) GPU_ID="NVIDIA $GPU" ;; esac
lab_model_def "$MODEL" || { echo "[ERROR] --model は ${LAB_MODELS// / か }: $MODEL" >&2; exit 2; }

if [ ! -d /Volumes/KIOXIA/r2r2r-data ]; then
    echo "[ERROR] SSD 未マウント: /Volumes/KIOXIA/r2r2r-data" >&2; exit 1
fi
if [ -z "$LOG" ]; then
    if [ -t 0 ]; then read -r -p "Log 番号 (例 Log17) > " LOG; fi
    [ -n "$LOG" ] || { echo "[ERROR] --log Log17 のように Log 番号を付けてください" >&2; exit 2; }
fi
LOGDIR="$LAB_ROOT/$LOG"
STATE="$LOGDIR/pod/lab_pod.json"

# ---- ターミナルの出力を画面に出しつつ <Log>/ に残す ----
if [ -z "${LAB_LOGGED:-}" ]; then
    mkdir -p "$LOGDIR/pod"
    LOGFILE="$LOGDIR/lab_start_$(date +%Y%m%d_%H%M%S).txt"
    echo "[LOG   ] ターミナルの出力を残します → $LOGFILE"
    if [ -t 0 ] && [ -t 1 ]; then
        # script は端末のまま記録する (camera_align.py・run_policy_client.py の入力待ちもそのまま使える)
        exec env LAB_LOGGED=1 script -q -a "$LOGFILE" "$0" "${ORIG_ARGS[@]}" --log "$LOG"
    fi
    export LAB_LOGGED=1
    exec > >(tee -a "$LOGFILE") 2>&1
fi
export PYTHONUNBUFFERED=1

lab_need_key "$RUNPOD_KEY_FILE" "Runpod の API キー"
lab_need_key "$HF_TOKEN_FILE" "Hugging Face の鍵"
[ -s "$POD_SSH_KEY" ] || { echo "[ERROR] Pod に入る SSH の鍵がありません: $POD_SSH_KEY" >&2; exit 1; }
[ -s "$POD_SSH_KEY.pub" ] || { echo "[ERROR] SSH の公開鍵がありません: $POD_SSH_KEY.pub" >&2; exit 1; }
ADD_TRIM=/Volumes/KIOXIA/r2r2r-data/lab/Log11/pod/add_trim_config.py
[ -f "$ADD_TRIM" ] || { echo "[ERROR] add_trim_config.py がありません: $ADD_TRIM" >&2; exit 1; }
RAW=/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[ -d "$RAW/$EPISODE" ] || { echo "[ERROR] お手本がありません: $RAW/$EPISODE" >&2; exit 1; }
lab_ssh_opts

HF_REPO=$MODEL_REPO                                                        # --model で選んだモデル (lab_lib.sh の lab_model_def)
OPENPI_COMMIT=$LAB_OPENPI_COMMIT                                          # lab_lib.sh (lab_train.sh と同じ)
CKPT=$MODEL_CKPT                                                           # 3c で hf download が置く場所 (trim は /workspace/ckpt/3999)
POLICY_CONFIG=$MODEL_CONFIG                                                # サーバーに渡す学習の設定
NORM_STATS=assets/local/$MODEL_DATA/norm_stats.json                        # 3f が写す assets (CKPT からの相対)
MODEL_TAG="$MODEL ($HF_REPO)"                                             # 記録に残すモデルの名前
LISTEN_WAIT=30; LISTEN_POLL=5    # プロセスはあるのに 8000 番で待っていないとき, 何秒待つか・何秒おきに見るか
TEMPLATE_ID=$LAB_TEMPLATE_ID
IMAGE=$LAB_IMAGE

# ---- 段階ごとの秒数 ----
T_NAMES=(); T_SECS=()
CUR_STAGE="(始める前)"
stage() { CUR_STAGE="$1"; STAGE_T0=$SECONDS; echo; echo "=== $1"; }
stage_done() { T_NAMES+=("$CUR_STAGE"); T_SECS+=($((SECONDS - STAGE_T0))); echo "    ($((SECONDS - STAGE_T0)) 秒)"; }
summary() {
    echo
    echo "=== かかった時間$([ "${REATTACH:-0}" = 1 ] && echo " ($LOG の Pod (id $POD_ID) につなぎ直し)")  モデル ${MODEL_TAG:-?}"
    local i
    for i in "${!T_NAMES[@]}"; do printf '  %-32s %6d 秒\n' "${T_NAMES[$i]}" "${T_SECS[$i]}"; done
}
POD_ID=""
save_model() {              # 記録 lab_pod.json に, サーバーを起動したモデルを残す (つなぎ直しのときに出す)
    jq --arg m "$MODEL" --arg r "$HF_REPO" '. + {model:$m, model_repo:$r}' "$STATE" > "$STATE.tmp" && mv "$STATE.tmp" "$STATE" \
        || echo "[WARN  ] $STATE にモデルを書けなかった"
}
fail() {
    echo
    echo "[STOP  ] 「${CUR_STAGE}」で止まりました: $*"
    if [ -n "$POD_ID" ]; then
        echo "[STOP  ] Pod は残っています (id $POD_ID). 消すなら ./lab_stop.sh --log $LOG"
    fi
    summary
    exit 1
}

TUNNEL_PID=""
LOCK="$LOGDIR/pod/.lab_start.lock"
LOCKED=0
cleanup() {
    if [ $LOCKED = 1 ] && [ "$(cat "$LOCK/pid" 2>/dev/null)" = "$$" ]; then rm -rf "$LOCK"; fi
    if [ -n "$TUNNEL_PID" ] && kill -0 "$TUNNEL_PID" 2>/dev/null; then
        kill "$TUNNEL_PID" 2>/dev/null
        echo "[TUNNEL] 8000 番のトンネルを閉じました"
    fi
}
trap cleanup EXIT
trap 'fail "Ctrl-C で止めました"' INT

# ---- 同じ Log の lab_start.sh を2つ同時に動かさない (mkdir は1つだけが成り立つ. 終わるときに外す) ----
# 印が残っていれば, 動いていてもいなくても止まる (自動で取り直すと, 2つが同時に取り直したときに両方進めるため)
if ! mkdir "$LOCK" 2>/dev/null; then
    other=$(cat "$LOCK/pid" 2>/dev/null)
    if [ -n "$other" ] && kill -0 "$other" 2>/dev/null; then
        echo "[ERROR] $LOG の lab_start.sh がもう動いています (pid $other). 二重に借りないよう止まります"
    else
        echo "[ERROR] $LOG の lab_start.sh の印が残っています (pid ${other:-?}・動いていないように見える). 二重に借りないよう止まります"
        echo "        ほかに $LOG の lab_start.sh が動いていないことを確かめてから, 印を消して打ち直してください:"
        echo "        rm -r $LOCK"
    fi
    exit 1
fi
LOCKED=1
echo $$ > "$LOCK/pid" || { echo "[ERROR] $LOCK/pid を書けない"; exit 1; }

echo "$LOG" > "$LAB_CURRENT"
echo "[LAB   ] $(date '+%Y-%m-%d %H:%M:%S')  GPU=$GPU_ID  episode=$EPISODE  log=$LOGDIR  $([ $DRY_RUN = 1 ] && echo 'ドライラン (アームなし)')"
echo "[MODEL ] --model $MODEL: $MODEL_DESC"
echo "         $HF_REPO → $CKPT  設定 $POLICY_CONFIG  assets local/$MODEL_DATA"

# ---- 1. Pod を作る (同じ Log の Pod が動いていれば, 作らずにつなぎ直す) ----
REATTACH=0
stage "1. Pod を作る"
if [ -f "$STATE" ]; then
    OLD_ID=$(jq -r '.id // ""' "$STATE" 2>/dev/null)
    if [ -z "$OLD_ID" ]; then
        lab_archive_state "$STATE" || fail "読めない $STATE を残せない"
        echo "[POD   ] $STATE に Pod の id がない. $LAB_ARCHIVED に名前を変えて残し, 新しく作ります"
    elif ! lab_pod_kind "$OLD_ID"; then
        fail "記録の Pod (id $OLD_ID) の様子を API で確かめられない (HTTP ${RUNPOD_HTTP:-?}). 二重に借りないよう止まります"
    else
        case "$LAB_POD_KIND" in
            running)
                REATTACH=1 ;;
            stopped)
                echo "[POD   ] $LOG の Pod (id $OLD_ID) は止まっています (状態 $LAB_POD_STATUS). 止まっていてもディスクの料金がかかるので,"
                echo "         ./lab_stop.sh と同じ消し方で消してから新しく作ります"
                "$HERE/lab_stop.sh" --log "$LOG" --skip-log || fail "止まっている Pod (id $OLD_ID) を消せない (上の lab_stop.sh の出力を見る)"
                lab_archive_state "$STATE" || fail "古い $STATE を残せない"
                echo "[POD   ] 古い記録は $LAB_ARCHIVED に残しました" ;;
            gone)
                lab_archive_state "$STATE" || fail "古い $STATE を残せない"
                echo "[POD   ] 記録の Pod (id $OLD_ID) は API で見ると残っていない${LAB_POD_STATUS:+ (状態 $LAB_POD_STATUS)}. 新しく作ります"
                echo "         古い記録は $LAB_ARCHIVED に残しました" ;;
        esac
    fi
fi
if [ $REATTACH = 1 ]; then
    POD_ID=$OLD_ID
    COST_PER_HR=$(jq -r '.costPerHr // 0' "$STATE")
    POD_T0=$(jq -r '.created_epoch // 0' "$STATE")
    OLD_GPU=$(jq -r '.gpu // ""' "$STATE")
    echo "[POD   ] $LOG の Pod (id $POD_ID) につなぎ直します (状態 RUNNING  GPU=$OLD_GPU  1時間 $COST_PER_HR ドル  作った時刻 $(jq -r '.created_at // "?"' "$STATE"))"
    [ "$OLD_GPU" = "$GPU_ID" ] || echo "[POD   ] --gpu は $GPU_ID ですが, 残っている Pod の $OLD_GPU をそのまま使います"
    echo "[POD   ] 記録の最後のモデル: $(jq -r '.model // "trim (記録なし. 依頼 52 より前は trim だけ)"' "$STATE")  今回: --model $MODEL"
    CUR_STAGE="1. Pod につなぎ直す"
else
    others=$(runpod_api GET /pods) || fail "Pod の一覧を API から取れない (HTTP $RUNPOD_HTTP)"
    lab_valid_pods "$others" || fail "Pod の一覧の答えが読めない. 二重に借りないよう止まります"
    same=$(jq -r --arg name "p2m-$LOG" '.[] | select(.name == $name and .desiredStatus != "TERMINATED") | .id' <<<"$others")
    if [ -n "$same" ]; then
        fail "記録にない $LOG の Pod (p2m-$LOG, id $(echo $same)) が残っている. 二重に借りないよう止まります (Runpod の画面か ./lab_stop.sh --check で確かめる)"
    fi
    n_others=$(jq 'length' <<<"$others")
    if [ "$n_others" -gt 0 ]; then
        echo "[WARN  ] ほかに Pod が $n_others 個あります (消し忘れなら ./lab_stop.sh --check で確かめる):"
        jq -r '.[] | "         \(.id)  \(.name)  \(.desiredStatus)  \(.costPerHr) ドル/時"' <<<"$others"
    fi
    body=$(jq -n --arg name "p2m-$LOG" --arg gpu "$GPU_ID" --arg tpl "$TEMPLATE_ID" --arg img "$IMAGE" \
              --arg pk "$(cat "$POD_SSH_KEY.pub")" \
              '{name:$name, templateId:$tpl, imageName:$img, gpuTypeIds:[$gpu], gpuCount:1,
                cloudType:"SECURE", containerDiskInGb:200, volumeInGb:0, ports:["22/tcp"],
                env:{PUBLIC_KEY:$pk}}')
    for attempt in 1 2 3 4 5; do
        resp=$(runpod_api POST /pods "$body") && break
        msg=$(jq -r '.error // .message // .' <<<"$resp" 2>/dev/null || echo "$resp")
        echo "[POD   ] 作れなかった ($attempt/5, HTTP $RUNPOD_HTTP): $msg"
        case "$msg" in
            *resources*|*"no longer any instances"*|*"not available"*|*"no instances"*)
                [ "$attempt" -lt 5 ] && { echo "[POD   ] 30 秒待って作り直します"; sleep 30; continue; } ;;
        esac
        fail "Pod を作れない: $msg"
    done
    POD_ID=$(jq -r '.id // ""' <<<"$resp")
    [ -n "$POD_ID" ] || fail "API の答えに Pod の id がない"
    COST_PER_HR=$(jq -r '.costPerHr // 0' <<<"$resp")
    POD_T0=$(date +%s)
    jq -n --arg id "$POD_ID" --arg gpu "$GPU_ID" --arg log "$LOG" --argjson cost "$COST_PER_HR" --argjson t0 "$POD_T0" \
          --arg model "$MODEL" --arg repo "$HF_REPO" \
          '{id:$id, gpu:$gpu, log:$log, costPerHr:$cost, created_epoch:$t0,
            created_at:($t0|strflocaltime("%Y-%m-%d %H:%M:%S")), model:$model, model_repo:$repo}' > "$STATE"
    echo "[POD   ] 作った: id $POD_ID  GPU=$GPU_ID  1時間 $COST_PER_HR ドル  (記録 $STATE)"
fi
stage_done

# ---- 2. Pod が立ち上がり, SSH の IP とポートが出るまで待つ ----
stage "2. Pod が立ち上がるまで待つ"
for _ in $(seq 1 90); do
    lab_pod_addr && break
    sleep 10
done
[ -n "${POD_IP:-}" ] && [ -n "${SSH_PORT:-}" ] || fail "15 分待っても SSH の IP とポートが出ない"
echo "[POD   ] SSH の IP とポートを API から$([ $REATTACH = 1 ] && echo '取り直した' || echo '受け取った') (<POD_IP>:<SSH_PORT>)"
for _ in $(seq 1 60); do
    LAB_TO=$LAB_TO_SHORT pod_ssh true >/dev/null && break
    sleep 5
done
pod_ssh 'echo "[POD   ] 入れた: $(hostname)"; nvidia-smi --query-gpu=name,memory.total --format=csv,noheader' \
    || fail "Pod に SSH で入れない"
stage_done

# ---- 3. Pod を準備する (つなぎ直しは, 済んでいるものを飛ばす) ----
NEED_PREP=1; NEED_CKPT=1; NEED_OPENPI=1; NEED_VENV=1
if [ $REATTACH = 1 ]; then
    stage "3. Pod の中を確かめる (つなぎ直し)"
    lab_pod_probe "$OPENPI_COMMIT" "$CKPT" "$NORM_STATS" || fail "Pod の中を確かめられない"
    yn() { [ "$1" = 1 ] && echo ある || echo ない; }
    if [ "$PROBE_CKPT" = 0 ]; then ckpt_msg=ない
    elif [ "$PROBE_MARK" = 1 ]; then ckpt_msg=ある
    else ckpt_msg="ある (取り終えた印 .lab_download_ok はない. 依頼 44 より前の lab_start.sh で取ったものなど)"; fi
    echo "[POD   ] serve_policy.py のプロセス: $(yn "$PROBE_SERVE")  8000 番の LISTEN: $(yn "$PROBE_LISTEN")"
    echo "[POD   ] モデル ($CKPT): $ckpt_msg  openpi ($OPENPI_COMMIT): $(yn "$PROBE_OPENPI")  .venv: $(yn "$PROBE_VENV")"
    if [ "$PROBE_SERVE" = 1 ]; then
        # 動いているサーバーが選んだモデルかを, プロセスの --policy.dir で見る (違えば起動し直さずに止まる. 依頼 52)
        lab_pod_serve_dir || fail "動いているサーバーのモデルを確かめられない (ssh に失敗)"
        if [ "$SERVE_DIR" != "$CKPT" ]; then
            other=$(lab_model_of_dir "$SERVE_DIR")
            echo "[POD   ] 動いているサーバーのモデル: --policy.dir=${SERVE_DIR:-(読めない)}${other:+ (--model $other)}  選んだモデル: --model $MODEL ($CKPT)"
            fail "残っている Pod のサーバーは別のモデル${other:+ (--model $other)}で動いています. 起動し直さずに止まります. そのモデルで続けるなら --model ${other:-?} で打ち直す. 選んだモデルに替えるなら ./lab_stop.sh --log $LOG で Pod を消してから打ち直す"
        fi
        echo "[POD   ] 動いているサーバーのモデル: --policy.dir=$SERVE_DIR (--model $MODEL と同じ)"
    fi
    if [ "$PROBE_SERVE" = 1 ] && [ "$PROBE_LISTEN" = 0 ]; then
        # 読み込みの途中かもしれないので少し待つ (エイイチの決め: 30 秒待ち, それでも待っていなければ起動し直す)
        echo "[POD   ] serve_policy.py は動いているが 8000 番で待っていない. 読み込み中かもしれないので $LISTEN_WAIT 秒まで待ちます"
        waited=0
        while [ $waited -lt $LISTEN_WAIT ]; do
            sleep $LISTEN_POLL; waited=$((waited + LISTEN_POLL))
            lab_pod_probe "$OPENPI_COMMIT" "$CKPT" "$NORM_STATS" || fail "Pod の中を確かめられない"
            [ "$PROBE_LISTEN" = 1 ] || [ "$PROBE_SERVE" = 0 ] && break
        done
        if [ "$PROBE_LISTEN" = 1 ]; then
            echo "[POD   ] $waited 秒で 8000 番で待ち始めました"
        else
            echo "[POD   ] $waited 秒待っても 8000 番で待っていない$([ "$PROBE_SERVE" = 0 ] && echo " (その間にプロセスが終わった)"). 起動し直します"
        fi
    fi
    if [ "$PROBE_SERVE" = 1 ] && [ "$PROBE_LISTEN" = 1 ]; then
        NEED_PREP=0
        echo "[POD   ] サーバーは 8000 番で待っています. 3b〜4 を飛ばします"
        pod_ssh "grep 'server listening' /workspace/logs/serve.log | tail -1"
    else
        echo "[POD   ] サーバーが動いていません. ないものだけやり直してサーバーを起動します"
        # 起動し直すと serve.log を空にするので, 前の serve.log を先に持ち帰る (落ちたわけを残す. だめでも続ける)
        dest="$LOGDIR/serve_before_restart_$(date +%Y%m%d_%H%M%S).log"
        if pod_scp "root@$POD_IP:/workspace/logs/serve.log" "$dest"; then
            echo "[LOG   ] 前の serve.log を持ち帰った → $dest ($(wc -l < "$dest" | tr -d ' ') 行)"
        else
            rm -f "$dest"; echo "[LOG   ] 前の serve.log は持ち帰れなかった (ないのかもしれない). 続けます"
        fi
        # 中身があっても印がなければ 3b・3c を流し直す (hf download は取り終えたファイルを取り直さない)
        [ "$PROBE_CKPT" = 1 ] && [ "$PROBE_MARK" = 1 ] && NEED_CKPT=0
        [ "$PROBE_OPENPI" = 1 ] && NEED_OPENPI=0
        [ "$PROBE_VENV" = 1 ] && [ $NEED_OPENPI = 0 ] && NEED_VENV=0     # openpi を取り直したら uv sync もやり直す
    fi
    stage_done
fi
skip() { T_NAMES+=("$1 (飛ばした)"); T_SECS+=(0); echo; echo "=== $1 → 飛ばす ($2)"; }

# スクリプトはつなぎ直しでも毎回送る (Mac 側の pi05/ が変わっているかもしれないため. 動いているサーバーには効かない)
stage "3a. スクリプトを送る"
pod_ssh "mkdir -p /workspace/r2r2r /workspace/logs /workspace/ckpt" || fail "Pod にフォルダを作れない"
LAB_TO=$LAB_TO_LONG pod_scp -r "$HERE/pi05" "root@$POD_IP:/workspace/r2r2r/" || fail "pi05/ を送れない"
pod_scp "$ADD_TRIM" "root@$POD_IP:/workspace/r2r2r/" || fail "add_trim_config.py を送れない"
stage_done

if [ $NEED_PREP = 0 ]; then
    skip "3b〜4. 準備とサーバーの起動" "サーバーが 8000 番で待っている"
else
    if [ $NEED_CKPT = 1 ]; then
        stage "3b. huggingface_hub を入れ, 鍵を渡す"
        LAB_TO=$LAB_TO_LONG pod_ssh "$(lab_cmd_hf_install)" || fail "huggingface_hub を入れられない"
        lab_send_hf_token || fail "Hugging Face の鍵を渡せない"     # 鍵は ssh の標準入力で渡す (lab_lib.sh)
        pod_ssh 'hf auth whoami' || fail "Hugging Face に入れない (鍵が違う: $HF_TOKEN_FILE を確かめる)"
        stage_done

        stage "3c. モデルを取る (hf download $HF_REPO)"
        LAB_TO=$LAB_TO_LONG pod_ssh "$(lab_cmd_ckpt "$HF_REPO" "$CKPT")" || fail "hf download に失敗"
        stage_done
    else
        skip "3b・3c. huggingface_hub・モデル" "モデルがある"
    fi

    if [ $NEED_OPENPI = 1 ]; then
        stage "3d. openpi を取る ($OPENPI_COMMIT)"
        LAB_TO=$LAB_TO_LONG pod_ssh "$(lab_cmd_openpi "$OPENPI_COMMIT")" || fail "openpi を取れない"
        stage_done
    else
        skip "3d. openpi を取る ($OPENPI_COMMIT)" "openpi がある"
    fi

    if [ $NEED_VENV = 1 ]; then
        stage "3e. uv sync"
        LAB_TO=$LAB_TO_LONG pod_ssh "$(lab_cmd_uv_sync)" || fail "uv sync に失敗"
        stage_done
    else
        skip "3e. uv sync" ".venv がある"
    fi

    # 3f は何度流しても同じ結果 (install_addon.py・add_trim_config.py とも二重に入れない) なので毎回流す
    stage "3f. addon・設定・assets ($POLICY_CONFIG)"
    pod_ssh "$(lab_cmd_addon_model "$MODEL")" || fail "addon・設定・assets に失敗"
    stage_done

    # ---- 3g・4. サーバーを tmux の中で起動し, 受け付けを始めるまで待つ ----
    stage "4. サーバーの起動"
    LAB_TO=$LAB_TO_LONG pod_ssh 'command -v tmux >/dev/null || { apt-get update -qq && apt-get install -y -qq tmux >/dev/null; }' \
        || fail "tmux を入れられない"
    # 起動し直すときは, 前の serve_policy.py が終わってから起動する (8000 番・GPU を空ける. 止めるのは別の ssh で)
    pod_ssh "$(lab_serve_stop_cmd)" || fail "前の serve_policy.py を止められない"
    pod_ssh ": > /workspace/logs/serve.log; \
             tmux new -d -s serve 'cd /workspace/openpi && source \$HOME/.local/bin/env && uv run scripts/serve_policy.py policy:checkpoint --policy.config=$POLICY_CONFIG --policy.dir=$CKPT 2>&1 | tee /workspace/logs/serve.log'" \
        || fail "serve_policy.py を起動できない"
    echo "[SERVE ] --model $MODEL: --policy.config=$POLICY_CONFIG --policy.dir=$CKPT"
    save_model
    listening=0
    for _ in $(seq 1 120); do
        if LAB_TO=$LAB_TO_SHORT pod_ssh "grep -q 'server listening on 0.0.0.0:8000' /workspace/logs/serve.log"; then listening=1; break; fi
        if ! LAB_TO=$LAB_TO_SHORT pod_ssh "tmux has-session -t serve 2>/dev/null"; then
            pod_ssh "tail -20 /workspace/logs/serve.log"
            fail "serve_policy.py が終わってしまった (上は serve.log の末尾)"
        fi
        sleep 10
    done
    [ $listening = 1 ] || { pod_ssh "tail -20 /workspace/logs/serve.log"; fail "20 分待っても server listening が出ない"; }
    pod_ssh "grep 'server listening' /workspace/logs/serve.log | tail -1"
    stage_done
fi

# ---- 5. トンネル ----
stage "5. Mac の 8000 番へのトンネル"
if lsof -nP -iTCP:$TUNNEL_PORT -sTCP:LISTEN >/dev/null 2>&1; then
    fail "Mac の $TUNNEL_PORT 番がすでに使われている (前のトンネルなら ./lab_stop.sh で閉じる)"
fi
ssh -N -L $TUNNEL_PORT:localhost:$TUNNEL_PORT "${SSH_OPTS[@]}" -o ExitOnForwardFailure=yes \
    -p "$SSH_PORT" "root@$POD_IP" 2>&1 | lab_mask &
sleep 1
TUNNEL_PID=$(lab_tunnel_pids | head -1)
for _ in $(seq 1 15); do
    lsof -nP -iTCP:$TUNNEL_PORT -sTCP:LISTEN >/dev/null 2>&1 && break
    sleep 1
done
lsof -nP -iTCP:$TUNNEL_PORT -sTCP:LISTEN >/dev/null 2>&1 || fail "トンネルを張れない"
echo "[TUNNEL] localhost:$TUNNEL_PORT → Pod の 8000 番 (pid $TUNNEL_PID. このスクリプトが終わると閉じる)"
stage_done

cd "$HERE" || fail "$HERE に移れない"

# ---- 6. デーモン ----
if [ $DRY_RUN = 0 ]; then
    stage "6. デーモンの状態"
    st=$("$HERE/tools/daemon_ctl.sh" status 2>&1); echo "$st"
    if ! grep -q ':5555 ' <<<"$st"; then
        echo "[DAEMON] 5555 番で受け付けていないので起動します"
        st=$("$HERE/tools/daemon_ctl.sh" start 2>&1); echo "$st"
        grep -q ':5555 ' <<<"$st" || fail "デーモンが 5555 番で受け付けない (tools/daemon_ctl.sh log で確かめる)"
    fi
    stage_done
fi

# ---- 7・8. 位置合わせと実機推論 (くり返せる) ----
# ここから先の Ctrl-C は, 動いている Python (位置合わせ・推論) だけを止める (このスクリプトは続ける)
trap ':' INT
round=0
while :; do
    round=$((round + 1))
    if [ $DRY_RUN = 0 ]; then
        stage "7. HOME → 爪を開く → 位置合わせ ($round 回目)"
        python3 dynamic_tracking_limits/place/place_modules/08_home_from_anywhere.py || fail "HOME に戻せない"
        python3 -c "from visual_servo_pick import get_daemon_socket, daemon_set_gripper, close_daemon_socket; get_daemon_socket(); daemon_set_gripper(0); close_daemon_socket()" \
            || fail "爪を開けない"
        python3 pi05/camera_align.py --raw-dir "$RAW" --episode "$EPISODE" --scale 5 --alpha 0.7
        rc=$?
        [ $rc = 0 ] || [ $rc = 130 ] || fail "camera_align.py が失敗 (終了コード $rc)"
        stage_done
        stage "8. 実機推論 ($round 回目)"
        python3 pi05/run_policy_client.py --raw-dir "$RAW" --steps-per-chunk 10 --via-stride 3 \
            --settle-retries 0 --state-from-target --execute --ignore-framing --model "$MODEL_TAG"
        rc=$?
    else
        stage "8. 推論のクライアント (ドライラン・--dry-keys. アームには触らない)"
        python3 pi05/run_policy_client.py --raw-dir "$RAW" --steps-per-chunk 10 --via-stride 3 \
            --settle-retries 0 --state-from-target --dry-keys --ignore-framing --model "$MODEL_TAG"
        rc=$?
    fi
    echo "[CLIENT] run_policy_client.py が終わった (終了コード $rc. モデル $MODEL_TAG)"
    stage_done
    if [ -t 0 ]; then
        read -r -p "もう1回やりますか? y=やる / 他=終わる > " again
    else
        again=n
    fi
    [ "$again" = y ] || break
done

# ---- 9. Pod は残す ----
summary
echo
echo "[LAB   ] Pod は残しています (id $POD_ID). 片付けは ./lab_stop.sh --log $LOG"
}

main "$@"; exit $?
```

### lab_train.sh（commit 284dfd7）

#### lab_train.sh

```bash
#!/bin/bash
# lab_train.sh — π0.5 の続きからの学習を Runpod で回し, 終わったらモデルと記録を持ち帰って Pod を消す (依頼 49)
#
#   ./lab_train.sh --log Log17             学習 → Hugging Face → KIOXIA → Pod を消す → Mac に通知
#   ./lab_train.sh --log Log17 --dry-run   Pod を借りずに流れだけ確かめる (API・SSH・hf・通知を差し替え,
#                                          書くのは一時フォルダだけ. 差し替えは lab_train_fakes.sh)
#   ./lab_train.sh --log Log17 --stop      この Log の学習の Pod のログを持ち帰って消す (Mac が途中で止まったときなど)
#
#   --gpu          最初に試す GPU (既定 A40). 借りられなければ RTX A6000 → L40S (どれも 48GB) を順に試す
#   --no-fallback  ほかの GPU を試さない (借りられなければ通知して終わる)
#   --limit-hours  上限の時間 (既定 5. Pod を作った時刻から数える). 超えたらログを持ち帰って消す
#   --model        作るモデル (依頼 57. 名前は lab_start.sh の --model と同じ. 既定 corr):
#                    corr  = 3999 から trim2_corr_v3 で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v1
#                    corr2 = corr の 1499 から trim2_corr_v4b で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v2
#                            (設定 pi05_mecharm_trim_corr_v4_lora. 3c で取るのは 3999 ではなく corr の 1499)
#                    corr3 = corr2 の 1499 から trim2_corr_v5 で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v3
#                            (依頼 97. 設定 pi05_mecharm_trim_corr_v5_lora. 3c で取るのは corr2 の 1499)
#   --steps / --init / --exp-name   学習 (既定 1500 / 続きの元の params / trim_corr_v3・trim_corr_v4・trim_corr_v5)
#                    続きの元: corr は /workspace/ckpt/3999/params, corr2 は /workspace/ckpt/corr_v1_1499/params,
#                              corr3 は /workspace/ckpt/corr_v2_1499/params
#   --poll-min     進みを見る間 (既定 5 分)
#   --stall-min    学習のログが何分書かれなければ「止まった」とするか (既定 30 分)
#
# 1. Pod を作る (名前 p2m-train-<Log>. lab_start.sh の p2m-<Log> とは別). 記録は lab/<Log>/train/lab_pod.json
# 2. SSH が通るまで待ち, Pod 側に上限の見張りを置く (上限 + 60 分で, Runpod が Pod に入れる Pod 用の鍵で自分を消す)
# 3. 準備 (lab_start.sh 3a〜3f と同じ. サーバーは起動しない)
# 4. tar を /workspace/data/ へ送り, pod_prep_trim_corr.sh <steps> <init>
# 5. 学習を setsid nohup で始める (Mac とのつながりが切れても止まらない). ログ /workspace/logs/train_<exp>.log,
#    GPU の様子は 30 秒おきに /workspace/logs/gpu_train.csv
# 6. 数分おきにステップ・loss を出す
# 7. 終わったら: 最後のチェックポイントを Hugging Face (非公開) へ上げて一覧を確かめる → ログと一覧を持ち帰る
#    → Pod を消す → KIOXIA の checkpoints/ へ Hugging Face から取り, Pod 側の一覧と名前・バイト数を比べる.
#    上げられなければ, Pod から KIOXIA へ scp で取ってから消す
# 8. 落ちた・止まった・上限を超えた・Ctrl-C: ログを持ち帰ってから消す
# 9. Mac に通知を出す. まとめは lab/<Log>/train/result_<時刻>.txt, ターミナルの出力は lab_train_<時刻>.txt
#    由来 (どのデータ・コード・設定で学習したか. provenance.py) を lab/<Log>/train/train_provenance.json に置き,
#    KIOXIA に新しく置けたチェックポイントと Hugging Face にも同じものを置く (依頼 54. 落ちた・止めたときも lab/ 側には置く)
#
# 鍵 (Runpod・Hugging Face) は KIOXIA の keys/ から読み, 画面・記録には出さない (lab_lib.sh).
# Pod 側の見張りには Mac の鍵を送らない.

set -u
set -o pipefail

# 中身は全部 main の中に置き, 最後の1行で呼ぶ (依頼 47 と同じ. 動いている途中でこのファイルが書き換わっても壊れない)
main() {
HERE="$(cd "$(dirname "$0")" && pwd)"
ORIG_ARGS=("$@")            # 由来に残す (依頼 54)
# shellcheck source=lab_lib.sh
. "$HERE/lab_lib.sh"

GPU=A40; FALLBACK=1; LIMIT_HOURS=5; STEPS=1500; INIT=""; EXP=""; MODEL=corr    # INIT・EXP の既定は --model から
POLL_MIN=5; STALL_MIN=30; LOG=""; DRY_RUN=0; MODE=train
while [ $# -gt 0 ]; do
    case "$1" in
        --log) lab_need_val Log18 "$@"; LOG="$2"; shift 2 ;;
        --gpu) lab_need_val A40 "$@"; GPU="$2"; shift 2 ;;
        --no-fallback) FALLBACK=0; shift ;;
        --limit-hours) lab_need_val 5 "$@"; LIMIT_HOURS="$2"; shift 2 ;;
        --model) lab_need_val corr2 "$@"; MODEL="$2"; shift 2 ;;
        --steps) lab_need_val 1500 "$@"; STEPS="$2"; shift 2 ;;
        --init) lab_need_val /workspace/ckpt/3999/params "$@"; INIT="$2"; shift 2 ;;
        --exp-name) lab_need_val trim_corr_v3 "$@"; EXP="$2"; shift 2 ;;
        --poll-min) lab_need_val 5 "$@"; POLL_MIN="$2"; shift 2 ;;
        --stall-min) lab_need_val 30 "$@"; STALL_MIN="$2"; shift 2 ;;
        --stop) MODE=stop; shift ;;
        --dry-run) DRY_RUN=1; shift ;;
        -h|--help) sed -n '2,39p' "$0"; exit 0 ;;
        *) echo "[ERROR] 知らない引数: $1  (./lab_train.sh --help)" >&2; exit 2 ;;
    esac
done
[ -n "$LOG" ] || { echo "[ERROR] --log Log17 のように Log 番号を付けてください" >&2; exit 2; }
lab_train_def "$MODEL" || { echo "[ERROR] --model は ${LAB_TRAIN_MODELS// / か }: $MODEL" >&2; exit 2; }
[ -n "$INIT" ] || INIT=$TRAIN_INIT
[ -n "$EXP" ] || EXP=$TRAIN_EXP
num() { awk -v x="$1" 'BEGIN { exit !(x ~ /^[0-9]+(\.[0-9]+)?$/ && x > 0) }'; }
for v in LIMIT_HOURS POLL_MIN STALL_MIN; do num "${!v}" || { echo "[ERROR] $v は 0 より大きい数: ${!v}" >&2; exit 2; }; done
[[ "$STEPS" =~ ^[1-9][0-9]*$ ]] || { echo "[ERROR] --steps は 1 以上の整数: $STEPS" >&2; exit 2; }
[[ "$EXP" =~ ^[A-Za-z0-9_.-]+$ ]] || { echo "[ERROR] --exp-name は英数字と _ . - だけ: $EXP" >&2; exit 2; }
[[ "$INIT" =~ ^[A-Za-z0-9_./-]+$ ]] || { echo "[ERROR] --init に使えない文字がある: $INIT" >&2; exit 2; }
case "$GPU" in *" "*) GPU_ID="$GPU" ;; *) GPU_ID="NVIDIA $GPU" ;; esac
secs() { awk -v x="$1" -v m="$2" 'BEGIN { printf "%d", x * m }'; }
LIMIT_SEC=$(secs "$LIMIT_HOURS" 3600); POLL_SEC=$(secs "$POLL_MIN" 60); STALL_SEC=$(secs "$STALL_MIN" 60)
WD_MARGIN_SEC=3600          # Pod 側の見張りは Mac の上限のこれだけあと (Mac が先に片付けるのがふつう)
WRAP_EXTEND_SEC=10800       # 片付け (上げる・持ち帰る) を始めたら, Pod 側の見張りを今から 3 時間あとへ延ばす
CREATE_WAIT=30; SSH_WAIT=10; SSH_TRIES=90; LOGIN_WAIT=5; LOGIN_TRIES=60; GONE_WAIT=5; GONE_TRIES=30
FETCH_TRIES=3; FETCH_WAIT=10
FALLBACK_GPUS=("NVIDIA RTX A6000" "NVIDIA L40S")    # A40 と同じ 48GB. この順に試す

DATA_ROOT=/Volumes/KIOXIA/r2r2r-data
NOTIFY_TITLE="lab_train $LOG"
if [ $DRY_RUN = 1 ]; then
    # shellcheck source=lab_train_fakes.sh
    . "$HERE/lab_train_fakes.sh"      # DATA_ROOT・LAB_ROOT・鍵を一時フォルダへ, API・SSH・hf・通知を偽物に, 待ちを短く
fi
CKPT_ROOT="$DATA_ROOT/checkpoints"
ADD_TRIM="$DATA_ROOT/lab/Log11/pod/add_trim_config.py"
use_model() {               # use_model corr|corr2|corr3 → 作るモデルで決まる値 (lab_lib.sh の lab_train_def. 依頼 57 / 97)
    lab_train_def "$1" || return 1
    DATA_NAME=$TRAIN_DATA                                   # corr: ..._trim2_corr_v3 / corr2: ..._trim2_corr_v4b (依頼 58) / corr3: ..._trim2_corr_v5
    TAR="$DATA_ROOT/datasets/$DATA_NAME/${DATA_NAME}_raw_episodes.tar"
    CONFIG=$TRAIN_CONFIG                                    # corr: pi05_mecharm_trim_corr_lora / corr2: ..._v4_lora / corr3: ..._v5_lora
    HF_REPO_IN=$TRAIN_REPO_IN                               # 続きの元 (corr: 3999 / corr2: corr の 1499 / corr3: corr2 の 1499. lab_start.sh と同じ)
    HF_REPO_OUT=$TRAIN_REPO_OUT                             # 上げる先 (非公開). 名前は checkpoints/ のフォルダ名に合わせる
    CKPT_NAME=$TRAIN_CKPT_NAME                              # KIOXIA の checkpoints/<これ>/<ステップ>/
    CKPT_IN=$TRAIN_CKPT_IN                                  # 3c で続きの元を置く場所
    CKPT_IN_KIOXIA="$CKPT_ROOT/$TRAIN_FROM_KIOXIA"          # HF_REPO_IN と同じもの (由来の指紋を取る. 依頼 54)
    PREP_VARIANT=$TRAIN_VARIANT                             # pod_prep_trim_corr.sh の 3 つ目 (v3 / v4 / v5)
}
use_model "$MODEL"
POD_EXP_DIR=/workspace/openpi/checkpoints/$CONFIG/$EXP
TRAIN_LOG=/workspace/logs/train_$EXP.log
GPU_LOG=/workspace/logs/gpu_train.csv
RUN_LOG=/workspace/logs/train_run.log
EXIT_FILE=/workspace/logs/train_exit_code
PREP_LOG=/workspace/logs/prep_trim_corr.log
LAST_STEP=$((STEPS - 1))      # openpi は 0 から数え, 最後のステップ (num_train_steps - 1) を必ず保存する

# ---- 書く前に KIOXIA のマウントを確かめる ----
if [ ! -d "$DATA_ROOT" ]; then
    echo "[ERROR] SSD 未マウント: $DATA_ROOT" >&2; exit 1
fi
LOGDIR="$LAB_ROOT/$LOG/train"
STATE="$LOGDIR/lab_pod.json"
mkdir -p "$LOGDIR" || { echo "[ERROR] $LOGDIR を作れない" >&2; exit 1; }
LOGFILE="$LOGDIR/lab_train_$(date +%Y%m%d_%H%M%S)$([ $MODE = stop ] && echo _stop).txt"
trap '' PIPE                # ターミナルが閉じて tee が終わっても, 書けないだけで続ける (Pod を消すところまで行く)
exec > >(tee -a "$LOGFILE") 2>&1
echo "[LOG   ] ターミナルの出力を残します → $LOGFILE"

lab_need_key "$RUNPOD_KEY_FILE" "Runpod の API キー"
lab_need_key "$HF_TOKEN_FILE" "Hugging Face の鍵"
[ -s "$POD_SSH_KEY" ] || { echo "[ERROR] Pod に入る SSH の鍵がありません: $POD_SSH_KEY" >&2; exit 1; }
[ -s "$POD_SSH_KEY.pub" ] || { echo "[ERROR] SSH の公開鍵がありません: $POD_SSH_KEY.pub" >&2; exit 1; }
lab_ssh_opts
pssh() { pod_ssh "$@" < /dev/null; }      # 標準入力を Pod へ渡さない ssh (時間の上限は pod_ssh. 長いものは LAB_TO=秒 を前に付ける)

# ---- 段階ごとの秒数 ----
T_NAMES=(); T_SECS=()
CUR_STAGE="(始める前)"; STAGE_T0=$SECONDS
POD_ID=""; POD_T0=""; COST_PER_HR=0; GPU_USED=""; WRAPPING=0; TRAIN_T0=""
stage() {                   # stage 名前 [nolimit]  (nolimit: 上限の確かめをしない. 見守りは様子を見てから上限を見る)
    CUR_STAGE="$1"; STAGE_T0=$SECONDS; echo; echo "=== $1  ($(date '+%H:%M:%S'))"
    if [ "${2:-}" != nolimit ] && [ $WRAPPING = 0 ] && [ -n "$POD_T0" ] && [ $(( $(date +%s) - POD_T0 )) -ge "$LIMIT_SEC" ]; then
        wrap_fail timeout "上限 $LIMIT_HOURS 時間を超えた (「$1」の前)"
    fi
}
stage_done() { T_NAMES+=("$CUR_STAGE"); T_SECS+=($((SECONDS - STAGE_T0))); echo "    ($((SECONDS - STAGE_T0)) 秒)"; }
summary() {
    local i
    echo; echo "=== かかった時間"
    for i in "${!T_NAMES[@]}"; do printf '  %-36s %6d 秒\n' "${T_NAMES[$i]}" "${T_SECS[$i]}"; done
}
hm() { local x=$1; [ "$x" -lt 0 ] && x=0; printf '%d:%02d' $((x / 3600)) $((x % 3600 / 60)); }

# ---- 由来 (依頼 54. provenance.py. 書けなくても学習の片付けは止めない) ----
PROV_PROCESS=""; PROV_ERROR=""; PROV_CKPT_DIR=""; PROV_HF=0; PROV_DONE=0
OPENPI_JSON='[]'            # 3f のあとに Pod の openpi のコミットと変更の有無を入れる
iso_of() { [ -n "${1:-}" ] && [ "$1" != 0 ] && date -r "$1" '+%Y-%m-%dT%H:%M:%S%z' | sed -E 's/([0-9]{2})$/:\1/'; }
prov_inputs() {             # 入力 (学習データ・続きの元 (corr は 3999・corr2 は corr の 1499・corr3 は corr2 の 1499)・設定を足すスクリプト) を spec の inputs の形で出す
    local init_ck=""
    case "$INIT" in "$CKPT_IN"/*) init_ck="$CKPT_IN_KIOXIA" ;; esac
    jq -n --arg data "$DATA_NAME" --arg data_dir "$DATA_ROOT/datasets/$DATA_NAME" \
        --arg init "$INIT" --arg init_ck "$init_ck" --arg init_id "${CKPT_IN_KIOXIA#"$CKPT_ROOT"/}" --arg repo_in "$HF_REPO_IN" \
        --arg add_trim "$ADD_TRIM" \
        '{inputs: ([{kind: "dataset", id: $data, path: $data_dir}]
                  + [{kind: "checkpoint", id: (if $init_ck == "" then (if $init == "" then null else $init end) else $init_id end),
                      path: (if $init_ck == "" then null else $init_ck end),
                      hf_repo: (if $init_ck == "" then null else $repo_in end), pod_path: (if $init == "" then null else $init end)}]
                  + [{kind: "config", id: "add_trim_config.py", path: $add_trim}])}'
}
PROV_CODE_ARGS=(--repo-dir "${LAB_PROV_REPO:-$HERE}" --code-path lab_train.sh --code-path lab_lib.sh --code-path provenance.py --code-path pi05)
prov_prepare() {            # 始める前にコードと入力の指紋を測って PROV_START に持つ (終わったときに測ると, 途中で変えたものになる)
    local out
    out=$(prov_inputs | lab_timeout 120 python3 "$HERE/provenance.py" prepare --data-root "$DATA_ROOT" "${PROV_CODE_ARGS[@]}" 2>/dev/null) \
        && jq -e '.code and .inputs' <<<"$out" >/dev/null 2>&1 && PROV_START=$(jq -c . <<<"$out") && return 0
    echo "[WARN  ] 始める時点のコードと入力の指紋を測れなかった (由来は終わったときに測る)"
    PROV_START=""
    return 1
}

prov_spec() {               # 由来の spec (lab_train.sh が知っていること) を JSON で出す
    local t_end rented start
    t_end=$(jq -r '.terminated_epoch // empty' "$STATE" 2>/dev/null)
    rented=$(jq -r '.rented_sec // empty' "$STATE" 2>/dev/null)
    start=${PROV_START:-}
    if [ -z "$start" ] && [ $MODE = stop ]; then
        # --stop で始めたときの記録がない (前の形の記録など): 止めた今のものを測らず「分からない」とする (codex R2)
        start=$(prov_inputs | jq -c '{code: {repo: "r2r2r-scripts", commit: null, dirty: null, error: "始めたときの記録がない (--stop)"},
                 inputs: [.inputs[] | . + {fingerprint: null, fingerprint_error: "始めたときの記録がない (--stop)", measured_at: null}]}') || return 1
    elif [ -z "$start" ]; then
        start=$(prov_inputs) || return 1        # 始める前に測れなかった: ここで測る (measured_at が終わったときの時刻になる)
    fi
    jq -n --argjson start "$start" --arg script lab_train.sh --argjson argv "$(printf '%s\0' ${ORIG_ARGS[@]+"${ORIG_ARGS[@]}"} | jq -Rs 'split("\u0000")[:-1]')" \
        --arg proc "$PROV_PROCESS" --arg err "$PROV_ERROR" --arg log "$LOG" --arg dry "$DRY_RUN" \
        --arg data "$DATA_NAME" --arg init "$INIT" --argjson openpi "$OPENPI_JSON" \
        --arg config "$CONFIG" --arg exp "$EXP" --arg steps "$STEPS" --arg limit "$LIMIT_HOURS" \
        --arg repo_out "$HF_REPO_OUT" --arg ckpt_name "$CKPT_NAME" --arg step "${CKPT_STEP:-}" --arg ck_dir "$PROV_CKPT_DIR" \
        --arg hf "${HF_MSG:-}" --arg kx "${KIOXIA_MSG:-}" --arg prov_hf "$PROV_HF" \
        --arg pod "$POD_ID" --arg gpu "$GPU_USED" --arg cph "$COST_PER_HR" --arg yen "$YEN_PER_USD" \
        --arg t0 "$(iso_of "$POD_T0")" --arg t1 "$(iso_of "$t_end")" --arg tt "$(iso_of "$TRAIN_T0")" --arg rented "$rented" \
        --arg cost_line "${COST_LINE:-}" \
        'def num: if . == "" then null else tonumber end; def str: if . == "" then null else . end;
        {producer: {script: $script, argv: $argv, dry_run: ($dry == "1")},
         external_code: $openpi,
         inputs: $start.inputs,
         code: ($start.code // null),
         outputs: (if $ck_dir == "" then [] else [{kind: "checkpoint", id: ($ckpt_name + "/" + $step), path: $ck_dir,
                   hf_repo: (if $prov_hf == "1" then $repo_out else null end)}] end),     # 指紋 files-v2 は由来のファイルを数えない (依頼 56)
         config: {name: $config, exp_name: $exp, num_train_steps: ($steps | num), init_params: ($init | str), data_name: $data,
                  limit_hours: ($limit | num), checkpoint_step: ($step | str)},
         compute: {provider: "runpod", pod_id: ($pod | str), gpu: ($gpu | str), started_at: ($t0 | str), train_started_at: ($tt | str),
                   ended_at: ($t1 | str), duration_sec: ($rented | num), cost_per_hr_usd: ($cph | num),
                   cost_usd: (if $rented == "" then null else (($rented | tonumber) / 3600 * ($cph | tonumber) * 100 | round / 100) end),
                   yen_per_usd: ($yen | num), cost_line: ($cost_line | str)},
         numbering: {series: "フィジカルAI研究室", series_log: $log, article_no: ""},
         status: {process: $proc, task: "not_applicable"},
         error: ($err | str),
         notes: {hugging_face: ($hf | str), kioxia: ($kx | str)}}'
}
PROV_TO=${LAB_PROV_TO:-300} # 由来を書く全体 (spec・書く・Hugging Face) の時間の上限 (通知を遅らせすぎない. codex R1・R2)
kill_group() {              # kill_group pgid → そのプロセスグループ (由来を書く子と, その子孫全部) に TERM, 2 秒たって残っていれば KILL
    # グループごと送るので, たどったあとに生まれた子孫も取りこぼさない. 番号の使い回しは, グループの全員が終わったあとにしか起きない
    # (KILL の前にまだ誰か残っているかを見る) (codex R3)
    kill -TERM -- "-$1" 2>/dev/null
    local i
    for i in 1 2 3 4; do pgrep -g "$1" >/dev/null 2>&1 || return 0; sleep 0.5; done
    kill -KILL -- "-$1" 2>/dev/null
    return 0
}
prov_write() {              # lab/<Log>/train/ (と, 今回新しく置いたチェックポイント・Hugging Face) に由来を置く. RESULT_LINES に1行足す
    local f pid t0 line
    [ $PROV_DONE = 1 ] && return 0
    PROV_DONE=1
    echo; echo "=== 由来 (train_provenance.json) を書く"
    [ -n "$PROV_PROCESS" ] || PROV_PROCESS=failed
    f=$(mktemp)
    # 自分のプロセスグループで流す (set -m). 時間切れのときにグループごと止める. 中の python と hf にはそれぞれ 120 秒の上限がある
    set -m
    ( trap - INT TERM HUP; prov_write_body ) > "$f" 2>&1 &
    pid=$!
    set +m
    t0=$SECONDS
    while kill -0 "$pid" 2>/dev/null && [ $((SECONDS - t0)) -lt "$PROV_TO" ]; do sleep 1; done
    if kill -0 "$pid" 2>/dev/null; then
        kill_group "$pid"; wait "$pid" 2>/dev/null
        echo "PROVLINE 由来: $PROV_TO 秒で書き終わらなかったので打ち切った (書けたものだけ残る)" >> "$f"
    else
        wait "$pid" 2>/dev/null
    fi
    grep -v '^PROVLINE ' "$f"
    line=$(sed -n 's/^PROVLINE //p' "$f" | tail -1); rm -f "$f"
    [ -n "$line" ] || line="由来: 書く途中で終わった"
    echo "[PROV  ] $line"
    RESULT_LINES=$(printf '%s\n%s' "${RESULT_LINES:-}" "$line")
}
prov_write_body() {         # 子のシェルで流す. 画面に出すものと, 最後に「PROVLINE <まとめの1行>」を出す
    local spec out lab_f ck_f="" outs line
    if [ ! -d "$DATA_ROOT" ] || [ ! -d "$LOGDIR" ]; then
        echo "PROVLINE 由来: KIOXIA がマウントされていない ($DATA_ROOT). 書かなかった"; return 1
    fi
    lab_f=$(dest_of train_provenance.json)
    outs=(--out "$lab_f")
    if [ -n "$PROV_CKPT_DIR" ] && [ -d "$PROV_CKPT_DIR" ]; then
        ck_f="$PROV_CKPT_DIR/train_provenance.json"
        outs+=(--out "$ck_f")
    fi
    spec=$(prov_spec) || spec=""
    if [ -z "$spec" ]; then
        echo "PROVLINE 由来: spec を作れなかった (jq). 書かなかった"; return 1
    fi
    out=$(printf '%s' "$spec" | lab_timeout 120 python3 "$HERE/provenance.py" write --data-root "$DATA_ROOT" "${outs[@]}" "${PROV_CODE_ARGS[@]}" \
              --secret-file "$RUNPOD_KEY_FILE" --secret-file "$HF_TOKEN_FILE" 2>&1)
    echo "$out" | sed 's/^/[PROV  ] /'
    line="由来: $([ -f "$lab_f" ] && echo "$lab_f" || echo "lab 側に書けなかった")"
    if [ -n "$ck_f" ]; then
        if grep -qxF -- "$ck_f" <<<"$out"; then line="$line ・ $ck_f"; else line="$line ・ チェックポイントには置けなかった"; fi
    fi
    if [ "$PROV_HF" = 1 ] && [ -f "$lab_f" ]; then
        # Hugging Face (今回 hf_upload_ckpt.py が上げたリポジトリ) にも同じものを足す. だめでも止めない
        if lab_hf_upload_file_mac "$HF_REPO_OUT" "$lab_f" train_provenance.json "lab_train.sh $LOG: train_provenance.json" | sed 's/^/[PROV  ] hf: /'; then
            line="$line ・ Hugging Face ($HF_REPO_OUT) にも置いた"
        else
            line="$line ・ Hugging Face には置けなかった"
        fi
    fi
    echo "PROVLINE $line"
}

# ---- 通知とまとめ ----
RESULT="$LOGDIR/result_$(date +%Y%m%d_%H%M%S).txt"
finish() {                  # finish 終了コード 通知の文 (まとめを書き, 通知を出して終わる)
    local rc="$1" msg="$2"
    FINISHED=1
    prov_write
    summary
    {
        echo "lab_train.sh $LOG  $(date '+%Y-%m-%d %H:%M:%S')"
        echo "結果: $msg"
        echo "${RESULT_LINES:-}"
    [ "${WD_OK:-1}" = 1 ] || echo "※ Pod 側の見張りは働いていなかった (${wd:-答えなし}). ./lab_stop.sh --check で Pod が残っていないか確かめる"
        echo "ターミナルの記録: $LOGFILE"
    } > "$RESULT"
    echo; echo "[RESULT] $msg"; echo "[RESULT] まとめ → $RESULT"
    lab_notify "$NOTIFY_TITLE" "$msg" "$rc"
    exit "$rc"
}
early_fail() {              # Pod を作る前に止まる (借りていないので消すものはない)
    echo; echo "[STOP  ] 「${CUR_STAGE}」で止まりました: $*"
    PROV_PROCESS=failed; PROV_ERROR="「${CUR_STAGE}」: $*"
    RESULT_LINES="Pod は借りていません"
    finish 1 "失敗: $* (Pod は借りていません)"
}

# ---- 同じ Log の lab_train.sh を2つ同時に動かさない (lab_start.sh と同じ形の印. 別のフォルダなのでぶつからない) ----
LOCK="$LOGDIR/.lab_train.lock"
LOCKED=0
FINISHED=0
cleanup() {
    # 思わぬところで終わった (set -u の止まりなど) ときも, 借りた Pod は残さない
    if [ "$FINISHED" = 0 ] && [ -n "$POD_ID" ]; then
        FINISHED=1
        echo "[ERROR] lab_train.sh が思わぬところで終わりました (「${CUR_STAGE}」). ログを持ち帰って Pod を消します"
        ( wrap_fail unexpected "lab_train.sh が思わぬところで終わった (「${CUR_STAGE}」)" )
    fi
    if [ $LOCKED = 1 ] && [ "$(cat "$LOCK/pid" 2>/dev/null)" = "$$" ]; then rm -rf "$LOCK"; fi
    [ -n "${CAFF_PID:-}" ] && kill "$CAFF_PID" 2>/dev/null
}
trap cleanup EXIT
if ! mkdir "$LOCK" 2>/dev/null; then
    other=$(cat "$LOCK/pid" 2>/dev/null)
    if [ -n "$other" ] && kill -0 "$other" 2>/dev/null; then
        echo "[ERROR] $LOG の lab_train.sh がもう動いています (pid $other). 止めるならそのターミナルで Ctrl-C (ログを持ち帰って Pod を消します)"
    else
        echo "[ERROR] $LOG の lab_train.sh の印が残っています (pid ${other:-?}・動いていないように見える)"
        echo "        ほかに動いていないことを確かめてから, 印を消して打ち直してください: rm -r $LOCK"
    fi
    exit 1
fi
LOCKED=1
echo $$ > "$LOCK/pid" || { echo "[ERROR] $LOCK/pid を書けない"; exit 1; }

save_state() {              # save_state jq の式 [--arg/--argjson ...]
    local f="$1"; shift
    jq "$@" "$f" "$STATE" > "$STATE.tmp" && mv "$STATE.tmp" "$STATE"
}

# ---- 片付け (持ち帰る・消す) ----
dest_of() {                 # dest_of 名前 → <train>/名前 (あれば 名前_<時刻>)
    local d="$LOGDIR/$1"
    if [ -e "$d" ]; then
        case "$1" in *.*) d="$LOGDIR/${1%.*}_$(date +%Y%m%d_%H%M%S).${1##*.}" ;; *) d="${d}_$(date +%Y%m%d_%H%M%S)" ;; esac
    fi
    echo "$d"
}

fetch_logs() {              # 学習のログ・GPU のログなどを持ち帰る. Pod に入れなければ FETCH_TRIES 回まで待ってやり直す
    local i f dest got=0 listing
    for i in $(seq 1 "$FETCH_TRIES"); do
        if lab_pod_addr && LAB_TO=$LAB_TO_SHORT pssh true >/dev/null; then
            for f in "$TRAIN_LOG" "$GPU_LOG" "$RUN_LOG" "$PREP_LOG" /workspace/logs/watchdog.log; do
                dest=$(dest_of "$(basename "$f")")
                if pod_scp "root@$POD_IP:$f" "$dest" >/dev/null; then
                    echo "[LOG   ] $(basename "$f") を持ち帰った → $dest ($(wc -l < "$dest" | tr -d ' ') 行)"; got=$((got + 1))
                else
                    rm -f "$dest"; echo "[LOG   ] $(basename "$f") は Pod にない (持ち帰れなかった)"
                fi
            done
            # Pod 側のチェックポイントの一覧 (全部のステップ. train_state も含む. 名前とバイト数)
            dest=$(dest_of pod_checkpoints.tsv)
            listing=$(LAB_TO=$LAB_TO_SHORT pssh ": lt=ls_all; cd $POD_EXP_DIR 2>/dev/null && find . -type f -printf '%P\t%s\n' | LC_ALL=C sort | sed 's/^/F\t/'")
            grep $'^F\t' <<<"$listing" | cut -f2- > "$dest"
            if [ -s "$dest" ]; then echo "[LOG   ] Pod 側のチェックポイントの一覧 → $dest ($(wc -l < "$dest" | tr -d ' ') ファイル)"
            else rm -f "$dest"; echo "[LOG   ] Pod 側にチェックポイントはない ($POD_EXP_DIR)"; fi
            FETCHED=$got
            return 0
        fi
        echo "[LOG   ] ($i/$FETCH_TRIES) Pod に入れない (状態 ${POD_STATUS:-?})"
        [ "$i" -lt "$FETCH_TRIES" ] && { echo "[LOG   ] $FETCH_WAIT 秒待ってやり直します"; sleep "$FETCH_WAIT"; }
    done
    FETCHED=0
    return 1
}

delete_pod() {              # Pod を消し (停止ではなく削除), 一覧からも消えたのを API で確かめ, 費用を出す. 消せなければ 1
    local tries pods gone=0 t_end s sent=1
    echo
    echo "=== Pod を消す (id $POD_ID)  ($(date '+%H:%M:%S'))"
    lab_pod_kind "$POD_ID"; local known=$?
    if [ $known = 0 ] && [ "$LAB_POD_KIND" = gone ]; then
        echo "[STOP  ] Pod (id $POD_ID) はもう API にない"
        t_end=$(jq -r '.terminated_epoch // empty' "$STATE" 2>/dev/null); [ -n "$t_end" ] || t_end=$(date +%s)
    else
        sent=0
        for tries in 1 2 3; do
            if runpod_api DELETE "/pods/$POD_ID" >/dev/null; then sent=1; break; fi
            echo "[STOP  ] ($tries/3) Pod を消せない (HTTP $RUNPOD_HTTP)"
            [ $tries -lt 3 ] && sleep "$GONE_WAIT"
        done
        t_end=$(date +%s)
        [ $sent = 1 ] && echo "[STOP  ] 削除 (Terminate) を送った ($(date -r "$t_end" '+%H:%M:%S'))"
    fi
    for _ in $(seq 1 "$([ "${sent:-1}" = 1 ] && echo "$GONE_TRIES" || echo 1)"); do
        if pods=$(runpod_api GET /pods) && lab_valid_pods "$pods" \
           && ! jq -e --arg id "$POD_ID" 'any(.[]; .id == $id and .desiredStatus != "TERMINATED")' <<<"$pods" >/dev/null; then
            gone=1; break
        fi
        sleep "$GONE_WAIT"
    done
    s=$((t_end - POD_T0))
    COST_LINE=$(lab_cost_line "$s" "$COST_PER_HR")
    save_state '. + {terminated_epoch:$t, terminated_at:($t|strflocaltime("%Y-%m-%d %H:%M:%S")), rented_sec:$s, gone_confirmed:($g == 1)}' \
        --argjson t "$t_end" --argjson s "$s" --argjson g "$gone"
    if [ $gone = 1 ]; then
        echo "[STOP  ] Pod (id $POD_ID) が消えたことを API で確かめた"
    else
        echo "[ERROR] Pod (id $POD_ID) が API の一覧に残っています$([ $sent = 0 ] && echo " (削除を受け付けなかった)"). Runpod の画面で確かめてください"
    fi
    echo "[COST  ] $COST_LINE  GPU=$GPU_USED"
    POD_GONE=$gone
    [ $gone = 1 ]
}

pod_left_msg() { echo "Pod (id $POD_ID) が残っているかもしれません. ./lab_train.sh --log $LOG --stop か Runpod の画面で消してください"; }

wrap_fail() {               # wrap_fail 種類 わけ → ログを持ち帰ってから Pod を消し, 通知して終わる (終了コード 1)
    local kind="$1" why="$2" logs
    WRAPPING=1; trap '' INT TERM HUP     # ここからは Ctrl-C でも止めない (Pod を消すところまで行く. 二重には入らない)
    echo; echo "[STOP  ] 「${CUR_STAGE}」で止めます: $why"
    case "$kind" in signal|stop|timeout) PROV_PROCESS=interrupted ;; *) PROV_PROCESS=failed ;; esac      # 由来 (依頼 54)
    [ "${TRAIN_DONE:-0}" = 1 ] && PROV_PROCESS=completed      # 学習は最後まで終わっていて, そのあとの片付けで止まった
    PROV_ERROR="$kind: 「${CUR_STAGE}」: $why"
    if [ -z "$POD_ID" ]; then RESULT_LINES="Pod は借りていません"; finish 1 "失敗: $why (Pod は借りていません)"; fi
    # 学習を止めてからログを持ち帰る (消す前に書き終わったログを取る. 止められなくても続ける)
    if [ "$kind" != crash ] && [ -n "$TRAIN_T0" ]; then
        LAB_TO=$LAB_TO_SHORT pssh "$(lab_train_kill_cmd)" >/dev/null
    fi
    if fetch_logs; then logs="ログを持ち帰った ($FETCHED 個)"; else logs="ログは持ち帰れなかった (Pod に入れない)"; fi
    if delete_pod; then
        RESULT_LINES=$(printf '%s\n' "わけ: $why" "$logs → $LOGDIR" "Pod (id $POD_ID) は消した (API で確かめた)" "$COST_LINE")
        finish 1 "失敗: $why. $logs. Pod は消しました. ${COST_LINE#借りていた時間 }"
    else
        RESULT_LINES=$(printf '%s\n' "わけ: $why" "$logs → $LOGDIR" "$(pod_left_msg)" "${COST_LINE:-}")
        finish 1 "失敗: $why. $logs. $(pod_left_msg)"
    fi
}

on_signal() { wrap_fail signal "$1 で止めた"; }
trap 'on_signal Ctrl-C' INT
trap 'on_signal "TERM (kill)"' TERM
trap 'on_signal "ターミナルが閉じた (HUP)"' HUP

# ---- KIOXIA の checkpoints/ に置く ----
POD_LIST=""                  # Pod 側の最後のチェックポイントの一覧 (パス<TAB>バイト数. train_state なし)
compare_with_pod() {        # compare_with_pod フォルダ → Pod 側の一覧と名前・バイト数が全部同じなら 0
    local mine d rc
    mine=$(mktemp)
    # 由来 (train_provenance.json) は Mac があとで足すので数えない (依頼 54)
    if ! { lab_list_files "$1" | { grep -v $'^train_provenance\.json\t' || true; }; } > "$mine" || [ ! -s "$POD_LIST" ]; then
        echo "[KIOXIA] 一覧を作れない (KIOXIA 側 $1 か Pod 側 $POD_LIST). 合ったとはしない"; rm -f "$mine"; return 1
    fi
    d=$(diff "$mine" "$POD_LIST"); rc=$?; rm -f "$mine"
    [ $rc = 0 ] && return 0          # diff の終了コードが 0 のときだけ「合った」(2 = 読めない も合わないとする)
    echo "[KIOXIA] Pod 側の一覧と合わない (diff の終了コード $rc. < KIOXIA / > Pod. 先頭 20 行):"
    head -20 <<<"$d" | sed 's/^/         /'
    return 1
}
place_ckpt() {              # place_ckpt hf|scp → KIOXIA の checkpoints/<CKPT_NAME>/<ステップ>/ に置いて確かめる. KIOXIA_MSG に結果
    local how="$1" parent="$CKPT_ROOT/$CKPT_NAME" dest tmp n bytes
    dest="$parent/$CKPT_STEP"
    if [ ! -d "$DATA_ROOT" ]; then KIOXIA_MSG="KIOXIA がマウントされていない (置けなかった)"; return 1; fi
    if [ -e "$dest" ] || [ -L "$dest" ]; then         # 前からあるものは上書き・移動・削除しない. 中身が同じかだけを見る
        if compare_with_pod "$dest"; then KIOXIA_MSG="$dest はもとからあり, Pod 側と全部同じ (書き換えていない)"; return 0; fi
        KIOXIA_MSG="$dest がもとからあり, Pod 側と違う. 上書きしなかった"; return 1
    fi
    mkdir -p "$parent" || { KIOXIA_MSG="$parent を作れない"; return 1; }
    tmp="$parent/.incoming_${CKPT_STEP}_$(date +%Y%m%d_%H%M%S)_$$"
    mkdir "$tmp" || { KIOXIA_MSG="$tmp を作れない"; return 1; }
    if [ "$how" = hf ]; then
        echo "[KIOXIA] Hugging Face ($HF_REPO_OUT) から取ります → $tmp"
        lab_hf_download_mac "$HF_REPO_OUT" "$tmp" || { KIOXIA_MSG="Hugging Face から取れなかった (途中のものは $tmp)"; return 1; }
    else
        echo "[KIOXIA] Pod から scp で取ります → $tmp"
        LAB_TO=$LAB_TO_LONG pod_scp -r "root@$POD_IP:$POD_CKPT/params" "root@$POD_IP:$POD_CKPT/assets" \
                   "root@$POD_IP:$POD_CKPT/_CHECKPOINT_METADATA" "$tmp/" \
            || { KIOXIA_MSG="Pod から scp で取れなかった (途中のものは $tmp)"; return 1; }
    fi
    rm -rf "${tmp:?}/.cache" "${tmp:?}/.gitattributes"     # hf download の印 (自分で作った一時フォルダの中だけ)
    rm -f "${tmp:?}/train_provenance.json"                 # Hugging Face にあった前の回の由来は持ってこない (今回のものをあとで置く. codex R1)
    if ! compare_with_pod "$tmp"; then KIOXIA_MSG="取ったものが Pod 側の一覧と合わない (そのまま $tmp に置いた)"; return 1; fi
    # mkdir は「なければ作る」を1回で行う (あれば失敗). 作れたら中身を移す (確かめと移すの間に別の実行が作っても上書きしない)
    if ! mkdir "$dest" 2>/dev/null; then KIOXIA_MSG="$dest が途中でできた. 上書きしない (取ったものは $tmp)"; return 1; fi
    # .DS_Store なども移す (codex R3). ._* (exFAT が付ける) は移さない (依頼 52): exFAT では ._<名前> は本体と一緒に
    # 動くので, 本体のあとに ._<名前> を mv すると「No such file」で止まる (依頼 49 の初回). 残った ._* は rm_incoming で片付ける
    if ! ( shopt -s dotglob nullglob; set --; for f in "$tmp"/*; do case "${f##*/}" in ._*) ;; *) set -- "$@" "$f" ;; esac; done
           [ $# = 0 ] || mv "$@" "$dest"/ ); then KIOXIA_MSG="$tmp から $dest へ移せない (途中まで移したかもしれない)"; return 1; fi
    rm_incoming "$tmp" || echo "[KIOXIA] $tmp に ._ 以外のものが残っている, または消せない. 消さずに残します"
    compare_with_pod "$dest" || { KIOXIA_MSG="$dest に移したあとの一覧が Pod 側と合わない"; return 1; }
    n=$(wc -l < "$POD_LIST" | tr -d ' '); bytes=$(awk -F'\t' '{s += $2} END {printf "%d", s}' "$POD_LIST")
    KIOXIA_MSG="$dest に置いた ($n ファイル $bytes バイト. Pod 側と名前・バイト数が全部合った)"
    PROV_CKPT_DIR="$dest"       # 今回新しく置いたものにだけ由来を置く (前からあるものには書き足さない. 依頼 54)
}

rm_incoming() {             # rm_incoming 一時フォルダ → ._* (exFAT が付ける) しか残っていなければ, それとフォルダを消す. ほかのものがあれば何もせず 1
    # 自分で作った .incoming_* にだけ使う (前からある .incoming_* には触らない)
    ( shopt -s dotglob nullglob
      for f in "$1"/*; do case "${f##*/}" in ._*) [ -f "$f" ] && [ ! -L "$f" ] || exit 1 ;; *) exit 1 ;; esac; done
      for f in "$1"/*; do rm -f "$f" || exit 1; done
      rmdir "$1" )
}

extend_watchdog() {         # Pod 側の見張りの時刻を, 今から WRAP_EXTEND_SEC あとへ延ばす (片付けの途中で消えないように)
    local t=$(( $(date +%s) + WRAP_EXTEND_SEC ))
    if LAB_TO=$LAB_TO_SHORT pssh ": lt=deadline; echo $t > /workspace/logs/.lab_deadline.tmp && mv /workspace/logs/.lab_deadline.tmp /workspace/logs/.lab_deadline" >/dev/null; then
        echo "[WATCH ] Pod 側の見張りを $(date -r "$t" '+%H:%M') まで延ばした (上げる・持ち帰る間に消えないように)"
    else
        echo "[WARN  ] Pod 側の見張りを延ばせなかった. 前の時刻 ($(date -r "$WD_AT" '+%H:%M')) を過ぎると片付けの途中でも Pod が消える"
    fi
}

wrap_success() {            # 学習が最後まで終わった: 上げる → 持ち帰る → 消す → KIOXIA へ
    local hf_ok=0 hf_line out k_ok=0 k_how="" logs msg rc
    WRAPPING=1; trap '' INT TERM HUP
    TRAIN_DONE=1                # このあとの片付けで止まっても, 由来は completed (codex R1)
    CKPT_STEP=$S_CKPT
    POD_CKPT="$POD_EXP_DIR/$CKPT_STEP"
    echo; echo "[TRAIN ] 学習が終わりました (最後のチェックポイント $CKPT_STEP, loss ${S_LOSS})"
    extend_watchdog

    stage "7a. Pod 側の一覧 ($CKPT_STEP)"
    POD_LIST=$(dest_of "pod_files_$CKPT_STEP.tsv")
    out=$(LAB_TO=$LAB_TO_SHORT pssh "$(lab_train_list_cmd "$POD_CKPT")"); local list_rc=$?
    grep $'^F\t' <<<"$out" | cut -f2- > "$POD_LIST"
    if [ $list_rc = 0 ] && [ -s "$POD_LIST" ] && grep -q $'^_CHECKPOINT_METADATA\t' "$POD_LIST"; then
        echo "[LIST  ] $(wc -l < "$POD_LIST" | tr -d ' ') ファイル $(awk -F'\t' '{s += $2} END {printf "%d", s}' "$POD_LIST") バイト (train_state なし) → $POD_LIST"
    else
        stage_done; wrap_fail list "Pod 側のチェックポイント ($POD_CKPT) の一覧を取れない"
    fi
    stage_done

    stage "7b. Hugging Face へ上げる (${HF_REPO_OUT}・非公開)"
    out=$(LAB_TO=$LAB_TO_LONG pssh "cd /workspace && HF_HUB_DISABLE_PROGRESS_BARS=1 python3 /workspace/r2r2r/pi05/openpi_addon/hf_upload_ckpt.py --dir $POD_CKPT --repo $HF_REPO_OUT --message 'lab_train.sh $LOG: $CONFIG $EXP step $CKPT_STEP'")
    echo "$out" | grep -v '^$'
    hf_line=$(grep '^HFUP ' <<<"$out" | tail -1)
    case "$hf_line" in "HFUP ok "*|"HFUP same "*) hf_ok=1 ;; esac
    HF_MSG=${hf_line#HFUP }; [ -n "$HF_MSG" ] || HF_MSG="答えがない (ssh が切れたなど)"
    stage_done

    if [ $hf_ok = 0 ]; then         # 上げられなければ, Pod を消す前に Pod から KIOXIA へ取る
        stage "7c. KIOXIA へ Pod から取る (Hugging Face に上げられなかったため)"
        extend_watchdog
        place_ckpt scp && k_ok=1; k_how=scp
        echo "[KIOXIA] $KIOXIA_MSG"
        stage_done
    fi

    stage "7d. ログを持ち帰る"
    if fetch_logs; then logs="ログを持ち帰った"; else logs="ログは持ち帰れなかった"; fi
    stage_done
    delete_pod; local gone=$?

    if [ $hf_ok = 1 ]; then         # Hugging Face にあるので, Pod を消してから取る (取れなくても Hugging Face に残る)
        stage "7e. KIOXIA へ Hugging Face から取る"
        place_ckpt hf && k_ok=1; k_how=hf
        echo "[KIOXIA] $KIOXIA_MSG"
        stage_done
    fi

    RESULT_LINES=$(printf '%s\n' "学習: $CONFIG --exp-name=$EXP  最後のチェックポイント $CKPT_STEP  最後の loss ${S_LOSS} (Step ${S_LOGSTEP})" \
        "Hugging Face ($HF_REPO_OUT): $HF_MSG" "KIOXIA: $KIOXIA_MSG" "$logs → $LOGDIR" \
        "$([ $gone = 0 ] && echo "Pod (id $POD_ID) は消した (API で確かめた)" || pod_left_msg)" "${COST_LINE:-}")
    if [ $hf_ok = 1 ] && [ $k_ok = 1 ] && [ $gone = 0 ]; then
        rc=0; msg="成功: 学習が終わりました (step $CKPT_STEP, loss ${S_LOSS}). Hugging Face と KIOXIA に置き, Pod を消しました. ${COST_LINE#借りていた時間 }"
    else
        rc=1; msg="学習は終わりました (step $CKPT_STEP) が,"
        [ $hf_ok = 0 ] && msg="$msg Hugging Face に上げられなかった ($HF_MSG)."
        if [ $k_ok = 1 ]; then msg="$msg KIOXIA には置いた ($k_how)."; else msg="$msg KIOXIA に置けなかった ($KIOXIA_MSG)."; fi
        if [ $gone = 0 ]; then msg="$msg Pod は消しました."; else msg="$msg $(pod_left_msg)"; fi
        PROV_ERROR="学習のあとの片付け: $msg"
    fi
    PROV_PROCESS=completed; [ $hf_ok = 1 ] && [ "${hf_line#HFUP ok }" != "$hf_line" ] && PROV_HF=1     # Hugging Face へは今回上げたときだけ足す
    finish "$rc" "$msg"
}

# ---- --stop: 残っている学習の Pod のログを持ち帰って消す ----
if [ $MODE = stop ]; then
    CUR_STAGE="--stop"
    [ -f "$STATE" ] || { echo "[ERROR] $STATE がありません (lab_train.sh で作った Pod の記録). ./lab_stop.sh --check で Pod の一覧を見る"; exit 1; }
    POD_ID=$(jq -r '.id // ""' "$STATE"); POD_T0=$(jq -r '.created_epoch // 0' "$STATE")
    COST_PER_HR=$(jq -r '.costPerHr // 0' "$STATE"); GPU_USED=$(jq -r '.gpu // ""' "$STATE")
    TRAIN_T0=$(jq -r '.train_started_epoch // ""' "$STATE")
    EXP=$(jq -r --arg e "$EXP" '.exp // $e' "$STATE")
    # 依頼 57: 作っていたモデル (記録がなければ corr. 依頼 57 より前は corr だけ) の値に合わせる. 設定の名前も記録から
    MODEL=$(jq -r '.model // "corr"' "$STATE")
    use_model "$MODEL" || { echo "[ERROR] $STATE のモデルが分からない: $MODEL"; exit 1; }
    CONFIG=$(jq -r --arg c "$CONFIG" '.config // $c' "$STATE")
    # 由来 (依頼 54. codex R1): 始めたときの設定・コード・入力を記録から戻す. 記録にないものは「分からない」(空) にする
    STEPS=$(jq -r '.steps // ""' "$STATE"); INIT=$(jq -r '.init // ""' "$STATE"); LIMIT_HOURS=$(jq -r '.limit_hours // ""' "$STATE")
    PROV_START=$(jq -c '.prov_start // empty' "$STATE")
    OPENPI_JSON=$(jq -c '.openpi // [{name: "openpi", commit: null, dirty: null, error: "記録にない (--stop)"}]' "$STATE") || OPENPI_JSON='[]'
    POD_EXP_DIR=/workspace/openpi/checkpoints/$CONFIG/$EXP; TRAIN_LOG=/workspace/logs/train_$EXP.log
    [ -n "$POD_ID" ] || { echo "[ERROR] $STATE に Pod の id がない"; exit 1; }
    echo "[STOP  ] $LOG の学習の Pod: id $POD_ID  GPU=$GPU_USED  作った時刻 $(jq -r '.created_at // "?"' "$STATE")"
    wrap_fail stop "--stop で止めた"
fi

echo "[LAB   ] $(date '+%Y-%m-%d %H:%M:%S')  学習  GPU=$GPU_ID$([ $FALLBACK = 1 ] && echo " (借りられなければ ${FALLBACK_GPUS[*]})")"
echo "         --model $MODEL ($HF_REPO_IN → $HF_REPO_OUT)  データ $DATA_NAME"
echo "         $CONFIG --exp-name=$EXP  steps=$STEPS  init=$INIT  上限 $LIMIT_HOURS 時間  記録 $LOGDIR$([ $DRY_RUN = 1 ] && echo '  ドライラン (Pod は借りない)')"

# ---- 0. 送るものと Mac の道具 ----
stage "0. 送るものと Mac の道具を確かめる"
[ -f "$TAR" ] || early_fail "tar がない: $TAR"
[ -f "$ADD_TRIM" ] || early_fail "add_trim_config.py がない: $ADD_TRIM"
[ -f "$HERE/pi05/openpi_addon/pod_prep_trim_corr.sh" ] && [ -f "$HERE/pi05/openpi_addon/hf_upload_ckpt.py" ] \
    || early_fail "pi05/openpi_addon/ の pod_prep_trim_corr.sh・hf_upload_ckpt.py がない"
lab_have_hf_cli || early_fail "Mac に hf (huggingface_hub) がない. KIOXIA へ取れないので借りずに止めます"
if [ -e "$CKPT_ROOT/$CKPT_NAME/$LAST_STEP" ]; then
    echo "[WARN  ] $CKPT_ROOT/$CKPT_NAME/$LAST_STEP がもうあります. 学習のあと上書きはせず, 中身が同じかだけを見ます"
fi
echo "[DATA  ] tar $(stat -f '%z' "$TAR" 2>/dev/null || wc -c < "$TAR") バイト  ($TAR)"
prov_prepare && echo "[PROV  ] 始める時点のコード ($(jq -r '.code.commit // "?" | .[0:7]' <<<"$PROV_START")$(jq -r 'if .code.dirty then "・変更あり" else "" end' <<<"$PROV_START")) と入力 $(jq '.inputs | length' <<<"$PROV_START") つの指紋を測った"
lab_keep_awake
stage_done

# ---- 1. Pod を作る ----
stage "1. Pod を作る"
if [ -f "$STATE" ]; then
    OLD_ID=$(jq -r '.id // ""' "$STATE" 2>/dev/null)
    if [ -n "$OLD_ID" ] && [ -z "$(jq -r '.gone_confirmed // empty | select(. == true)' "$STATE" 2>/dev/null)" ]; then
        lab_pod_kind "$OLD_ID" || early_fail "前の学習の Pod (id $OLD_ID) の様子を API で確かめられない (HTTP ${RUNPOD_HTTP:-?}). 二重に借りないよう止めます"
        case "$LAB_POD_KIND" in
            running|stopped)
                early_fail "前の学習の Pod (id ${OLD_ID}・$LAB_POD_STATUS) が残っている. ./lab_train.sh --log $LOG --stop で持ち帰って消してから打ち直す" ;;
        esac
    fi
    lab_archive_state "$STATE" || early_fail "古い $STATE を残せない"
    echo "[POD   ] 前の記録は $LAB_ARCHIVED に残しました"
fi
others=$(runpod_api GET /pods) || early_fail "Pod の一覧を API から取れない (HTTP $RUNPOD_HTTP)"
lab_valid_pods "$others" || early_fail "Pod の一覧の答えが読めない. 二重に借りないよう止めます"
same=$(jq -r --arg name "p2m-train-$LOG" '.[] | select(.name == $name and .desiredStatus != "TERMINATED") | .id' <<<"$others")
[ -z "$same" ] || early_fail "記録にない p2m-train-$LOG の Pod (id $(echo $same)) が残っている. 二重に借りないよう止めます (./lab_stop.sh --check)"
n_others=$(jq 'length' <<<"$others")
if [ "$n_others" -gt 0 ]; then
    echo "[WARN  ] ほかに Pod が $n_others 個あります (消し忘れなら ./lab_stop.sh --check で確かめる):"
    jq -r '.[] | "         \(.id)  \(.name)  \(.desiredStatus)  \(.costPerHr) ドル/時"' <<<"$others"
fi
GPUS=("$GPU_ID")
if [ $FALLBACK = 1 ]; then for g in "${FALLBACK_GPUS[@]}"; do [ "$g" = "$GPU_ID" ] || GPUS+=("$g"); done; fi
for gi in "${!GPUS[@]}"; do
    g=${GPUS[$gi]}
    tries=$([ "$gi" = 0 ] && echo 5 || echo 2)
    echo "[POD   ] $g を借ります (空きがなければ $CREATE_WAIT 秒おきに $tries 回まで)"
    body=$(jq -n --arg name "p2m-train-$LOG" --arg gpu "$g" --arg tpl "$LAB_TEMPLATE_ID" --arg img "$LAB_IMAGE" \
              --arg pk "$(cat "$POD_SSH_KEY.pub")" \
              '{name:$name, templateId:$tpl, imageName:$img, gpuTypeIds:[$gpu], gpuCount:1,
                cloudType:"SECURE", containerDiskInGb:200, volumeInGb:0, ports:["22/tcp"],
                env:{PUBLIC_KEY:$pk}}')
    busy=0
    for attempt in $(seq 1 "$tries"); do
        out=$(mktemp)
        runpod_api POST /pods "$body" > "$out"; ok=$?        # $(...) に入れない (RUNPOD_HTTP を残す)
        resp=$(cat "$out"); rm -f "$out"
        if [ $ok = 0 ]; then
            POD_ID=$(jq -r '.id // ""' <<<"$resp")
            [ -n "$POD_ID" ] || early_fail "API の答えに Pod の id がない"
            COST_PER_HR=$(jq -r '.costPerHr // 0' <<<"$resp"); GPU_USED=$g
            break 2
        fi
        msg=$(jq -r '.error // .message // .' <<<"$resp" 2>/dev/null || echo "$resp")
        echo "[POD   ] 作れなかった ($g $attempt/$tries, HTTP $RUNPOD_HTTP): $msg"
        case "$msg" in
            *resources*|*"no longer any instances"*|*"not available"*|*"no instances"*) busy=1 ;;
            *) early_fail "Pod を作れない: $msg" ;;
        esac
        [ "$attempt" -lt "$tries" ] && { echo "[POD   ] $CREATE_WAIT 秒待って作り直します"; sleep "$CREATE_WAIT"; }
    done
    [ $busy = 1 ] && echo "[POD   ] $g は空きがない"
done
[ -n "$POD_ID" ] || early_fail "どの GPU (${GPUS[*]}) も借りられなかった"
POD_T0=$(date +%s)
jq -n --arg id "$POD_ID" --arg gpu "$GPU_USED" --arg log "$LOG" --argjson cost "$COST_PER_HR" --argjson t0 "$POD_T0" \
      --arg exp "$EXP" --argjson steps "$STEPS" --arg config "$CONFIG" --argjson lim "$LIMIT_SEC" \
      --arg init "$INIT" --arg lh "$LIMIT_HOURS" --arg ps "${PROV_START:-}" --arg model "$MODEL" \
      '{id:$id, kind:"train", model:$model, gpu:$gpu, log:$log, costPerHr:$cost, created_epoch:$t0,
        created_at:($t0|strflocaltime("%Y-%m-%d %H:%M:%S")), config:$config, exp:$exp, steps:$steps,
        limit_sec:$lim, limit_at:(($t0 + $lim)|strflocaltime("%Y-%m-%d %H:%M:%S")),
        init:$init, limit_hours:$lh, prov_start:(if $ps == "" then null else ($ps | fromjson) end)}' > "$STATE"
echo "[POD   ] 作った: id $POD_ID  GPU=$GPU_USED  1時間 $COST_PER_HR ドル  (記録 $STATE)"
echo "[POD   ] 上限: $(date -r $((POD_T0 + LIMIT_SEC)) '+%m-%d %H:%M') ($LIMIT_HOURS 時間) を過ぎたら, ログを持ち帰って消します"
stage_done

# ---- 2. SSH が通るまで待ち, Pod 側に上限の見張りを置く ----
stage "2. Pod が立ち上がるまで待つ"
for _ in $(seq 1 "$SSH_TRIES"); do lab_pod_addr && break; sleep "$SSH_WAIT"; done
[ -n "${POD_IP:-}" ] && [ -n "${SSH_PORT:-}" ] || wrap_fail prep "待っても SSH の IP とポートが出ない"
echo "[POD   ] SSH の IP とポートを API から受け取った (<POD_IP>:<SSH_PORT>)"
for _ in $(seq 1 "$LOGIN_TRIES"); do LAB_TO=$LAB_TO_SHORT pssh true >/dev/null && break; sleep "$LOGIN_WAIT"; done
LAB_TO=$LAB_TO_SHORT pssh 'echo "[POD   ] 入れた: $(hostname)"; nvidia-smi --query-gpu=name,memory.total --format=csv,noheader' \
    || wrap_fail prep "Pod に SSH で入れない"
WD_AT=$((POD_T0 + LIMIT_SEC + WD_MARGIN_SEC))
LAB_TO=$LAB_TO_SHORT pssh "mkdir -p /workspace/r2r2r /workspace/logs /workspace/ckpt /workspace/data" || wrap_fail prep "Pod にフォルダを作れない"
lab_watchdog_script | LAB_TO=$LAB_TO_SHORT pod_ssh "cat > /workspace/r2r2r/lab_watchdog.sh" || wrap_fail prep "見張りのスクリプトを置けない"
out=$(LAB_TO=$LAB_TO_SHORT pssh "$(lab_watchdog_start_cmd "$WD_AT")")      # 依頼 52: 初回はここで ssh が返らなかった
wd=$(grep -E '^WATCHDOG ' <<<"$out" | tail -1)
WD_OK=0
case "$wd" in
    "WATCHDOG run=1 pod=1 key=1"*)
        WD_OK=1
        echo "[WATCH ] Pod 側の見張りを置いた: $(date -r "$WD_AT" '+%m-%d %H:%M') を過ぎたら Pod が自分を消す (Pod 用の鍵. Mac の鍵は送っていない)" ;;
    *)
        # 止めずに続ける (Pod 用の鍵が入らない Pod だと毎回学習できなくなるため). 画面・通知・まとめで目立たせる
        echo "[WARN  ] Pod 側の見張りが働かないかもしれない (${wd:-答えなし}). Mac 側の上限だけで消します. Mac を止めないでください" ;;
esac
save_state '. + {watchdog_epoch:$w, watchdog:$s}' --argjson w "$WD_AT" --arg s "${wd:-なし}"
stage_done

# ---- 3. 準備 (lab_start.sh 3a〜3f と同じ. サーバーは起動しない) ----
stage "3a. スクリプトを送る"
LAB_TO=$LAB_TO_LONG pod_scp -r "$HERE/pi05" "root@$POD_IP:/workspace/r2r2r/" || wrap_fail prep "pi05/ を送れない"
pod_scp "$ADD_TRIM" "root@$POD_IP:/workspace/r2r2r/" || wrap_fail prep "add_trim_config.py を送れない"
stage_done
stage "3b. huggingface_hub を入れ, 鍵を渡す"
LAB_TO=$LAB_TO_LONG pssh "$(lab_cmd_hf_install)" || wrap_fail prep "huggingface_hub を入れられない"
lab_send_hf_token || wrap_fail prep "Hugging Face の鍵を渡せない"
LAB_TO=$LAB_TO_SHORT pssh 'hf auth whoami' || wrap_fail prep "Hugging Face に入れない (鍵が違う: $HF_TOKEN_FILE を確かめる)"
stage_done
stage "3c. 続きから学習するモデルを取る ($HF_REPO_IN → $CKPT_IN)"
LAB_TO=$LAB_TO_LONG pssh "$(lab_cmd_ckpt "$HF_REPO_IN" "$CKPT_IN")" || wrap_fail prep "hf download に失敗"
stage_done
stage "3d. openpi を取る ($LAB_OPENPI_COMMIT)"
LAB_TO=$LAB_TO_LONG pssh "$(lab_cmd_openpi "$LAB_OPENPI_COMMIT")" || wrap_fail prep "openpi を取れない"
stage_done
stage "3e. uv sync"
LAB_TO=$LAB_TO_LONG pssh "$(lab_cmd_uv_sync)" || wrap_fail prep "uv sync に失敗"
stage_done
stage "3f. addon・設定・assets"
pssh "$(lab_cmd_addon_train "$MODEL")" || wrap_fail prep "addon・設定・assets に失敗"
# 由来 (依頼 54): 学習に使う Pod の openpi のコミットと変更の有無 (addon・設定を入れたあと). 取れなくても続ける
# git status が最後まで通ったときだけ OPENPI_END を出す. ssh の終了コードと両方を見る (途中で切れたものを「変更なし」にしない. codex R1)
out=$(LAB_TO=$LAB_TO_SHORT pssh ": lt=openpi_rev; cd /workspace/openpi && c=\$(git rev-parse HEAD) && s=\$(git status --porcelain=v1 --untracked-files=all) && echo \"OPENPI \$c\" && printf '%s\\n' \"\$s\" | sed -n 's/^\\(..\\) /OPF \\1 /p' && echo OPENPI_END"); orc=$?
if oc=$(sed -n 's/^OPENPI \([0-9a-f]\{40\}\)$/\1/p' <<<"$out" | tail -1) && [ -n "$oc" ] && [ $orc = 0 ] && grep -qx OPENPI_END <<<"$out"; then
    OPENPI_JSON=$(grep '^OPF ' <<<"$out" | head -200 | cut -c8- | jq -Rn --arg c "$oc" --arg want "$LAB_OPENPI_COMMIT" \
        '[inputs] as $f | [{name: "openpi", commit: $c, dirty: ($f | length > 0), dirty_files: $f, expected_commit: $want}]') \
        || OPENPI_JSON='[]'
    echo "[PROV  ] openpi $oc ($(jq -r '.[0].dirty_files | length' <<<"$OPENPI_JSON") ファイル変更・addon と設定を入れたあと)"
else
    OPENPI_JSON=$(jq -n --arg want "$LAB_OPENPI_COMMIT" '[{name: "openpi", commit: null, dirty: null, expected_commit: $want, error: "Pod から読めなかった"}]')
    echo "[WARN  ] Pod の openpi のコミットを読めなかった (由来には「読めなかった」と残す)"
fi
save_state '. + {openpi:$o}' --argjson o "$OPENPI_JSON"       # --stop で戻す (codex R2)
stage_done

# ---- 4. データを送り, 学習の準備 ----
stage "4a. tar を送る"
LAB_TO=$LAB_TO_LONG pod_scp "$TAR" "root@$POD_IP:/workspace/data/" || wrap_fail prep "tar を送れない"
LAB_TO=$LAB_TO_SHORT pssh "ls -l /workspace/data/$(basename "$TAR")" || wrap_fail prep "送った tar が見えない"
stage_done
stage "4b. pod_prep_trim_corr.sh $STEPS $INIT $PREP_VARIANT"
LAB_TO=$LAB_TO_LONG pssh "set -o pipefail; bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh $STEPS $INIT $PREP_VARIANT 2>&1 | tee $PREP_LOG" \
    || wrap_fail prep "pod_prep_trim_corr.sh に失敗 (上の出力を見る)"
stage_done

# ---- 5. 学習を始める ----
stage "5. 学習を始める"
out=$(LAB_TO=$LAB_TO_SHORT pssh "$(lab_train_start_cmd "$CONFIG" "$EXP" "$TRAIN_LOG" "$GPU_LOG" "$RUN_LOG" "$EXIT_FILE")")
grep -q '^STARTED' <<<"$out" || wrap_fail prep "学習を始められない ($(tail -3 <<<"$out" | tr '\n' ' '))"
TRAIN_T0=$(date +%s)
save_state '. + {train_started_epoch:$t, train_started_at:($t|strflocaltime("%Y-%m-%d %H:%M:%S"))}' --argjson t "$TRAIN_T0"
echo "[TRAIN ] 学習を始めました (Pod の中で setsid nohup. Mac とのつながりが切れても続く)"
echo "         ログ $TRAIN_LOG  GPU $GPU_LOG (30 秒おき)"
stage_done

# ---- 6. 見守る ----
stage "6. 学習を見守る ($POLL_MIN 分おき)" nolimit       # 上限は下のループで, 終わっているかを見てから
told=0; ssh_ng=0
while :; do
    # 上限を超えていても, 先に様子を見る (確かめる間に学習が終わっていれば, 消さずに持ち帰る)
    out=$(LAB_TO=$LAB_TO_SHORT pssh "$(lab_train_status_cmd "$TRAIN_LOG" "$EXIT_FILE" "$POD_EXP_DIR" "$STEPS")")
    now=$(date +%s)
    if lab_train_parse_status "$out" && [ "$S_EXIT" = 0 ] && [ "$S_CKPT" = "$LAST_STEP" ] && [ "$S_CM" = 1 ]; then
        :       # 終わっている → 下で成功へ
    elif [ $((now - POD_T0)) -ge "$LIMIT_SEC" ]; then
        wrap_fail timeout "上限 $LIMIT_HOURS 時間を超えた (学習を始めてから $(hm $((now - TRAIN_T0))))"
    fi
    if lab_train_parse_status "$out"; then
        ssh_ng=0
        step=$S_BAR; [ "$step" = - ] && step=$S_LOGSTEP
        printf '[TRAIN ] %s  step %s/%s  loss %s%s  経過 %s  上限まで %s\n' "$(date '+%H:%M')" "$step" "$STEPS" "$S_LOSS" \
            "$([ "$S_LOGSTEP" != - ] && echo " (Step $S_LOGSTEP の行)")" "$(hm $((now - TRAIN_T0)))" "$(hm $((POD_T0 + LIMIT_SEC - now)))"
        if [ "$S_EXIT" != - ]; then
            if [ "$S_EXIT" = 0 ] && [ "$S_CKPT" = "$LAST_STEP" ] && [ "$S_CM" = 1 ]; then
                stage_done; wrap_success
            fi
            wrap_fail crash "学習が終わったが, 終了コード ${S_EXIT}・最後のチェックポイント $S_CKPT (待っていたのは $LAST_STEP)"
        fi
        [ "$S_RUN" = 0 ] && wrap_fail crash "学習のプロセスが終わりの印を書かずに消えた"
        if [ "$S_AGE" != - ] && [ "$S_AGE" -ge "$STALL_SEC" ]; then
            wrap_fail stall "学習のログが $((S_AGE / 60)) 分書かれていない (止まった. $STALL_MIN 分で止めると決めている)"
        fi
        if [ $told = 0 ]; then
            told=1
            echo
            echo "========================================================================"
            echo " 学習は Pod の中で回っています. ここまで来れば寝てかまいません."
            echo " このターミナルは閉じない・Mac のふたは閉じない (電源につないだまま)."
            echo " 終わると通知が出ます. まとめは $LOGDIR/result_*.txt"
            [ $WD_OK = 1 ] || echo " ※ Pod 側の見張りがない. Mac が止まると Pod が残る (朝に ./lab_stop.sh --check で確かめる)"
            echo "========================================================================"
            echo
        fi
    else
        ssh_ng=$((ssh_ng + 1))
        echo "[WARN  ] $(date '+%H:%M') Pod の様子を見られない ($ssh_ng 回続けて)"
        if lab_pod_kind "$POD_ID" && [ "$LAB_POD_KIND" != running ]; then
            wrap_fail gone "Pod が動いていない (API の状態 ${LAB_POD_STATUS:-なし})"
        fi
        lab_pod_addr || true      # IP とポートが変わっていれば取り直す
    fi
    sleep "$POLL_SEC"
done
}

main "$@"; exit $?
```

### commit 284dfd7 の学習まわりの差分（lab_*.sh・設定・テスト）

#### commit 284dfd7 の差分（lab_lib.sh・lab_start.sh・lab_train.sh・lab_train_fakes.sh・テスト・設定）

```diff
commit 284dfd731e64353c996486432811aebb5ee47ea0
Author: Eiichi Shimazu <studio@adaptdesign.jp>
Date:   Fri Nov 6 19:51:31 2026 +0900

    11-06 のゲームパッドで直した成功 5 本を diag_joint_hold.py で点検できるようにし (rpc 行 send_angle / stop を log.jsonl の pad の行と組にして指令を作る・長押しの関節は見ない・送ったらすぐ返る daemon の区間は向かう途中を遅れに数えず指令から離れた分 away を見る. 11-01 の結果は変わらない), 外れのない 183926・184452・184650・184906 を corr_v4b の 7 本に足した corr_v5 で, corr2 の 1499 から続きを学習する lab_train.sh / lab_start.sh の --model corr3 を足す (設定 pi05_mecharm_trim_corr_v5_lora・上げる先 pi05-mecharm-trim-corr-lora-v3). build_correction_episodes がゲームパッドの区間を扱えることを test_pad_build.py で確かめる (依頼 97. codex 3 回)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/lab_lib.sh b/lab_lib.sh
index f890316..e9a0db7 100644
--- a/lab_lib.sh
+++ b/lab_lib.sh
@@ -275,11 +275,14 @@ lab_cmd_addon() {           # 3f. 何度流しても同じ結果 (install_addon.
 # corr の学習の設定 (pi05_mecharm_trim_corr_lora) は lab_train.sh の学習と同じ値で足す (add_trim_corr_config.py は
 # 入っている設定と --steps / --init が違うと止まる. 推論では weight_loader は使わない)
 # corr2 (依頼 57) は corr_v1_1499 から trim2_corr_v4 で続きを学習したもの (設定 pi05_mecharm_trim_corr_v4_lora)
-LAB_MODELS="trim corr corr2"
+# corr3 (依頼 97) は corr_v2_1499 から trim2_corr_v5 で続きを学習したもの (設定 pi05_mecharm_trim_corr_v5_lora)
+LAB_MODELS="trim corr corr2 corr3"
 LAB_CORR_STEPS=1500
 LAB_CORR_INIT=/workspace/ckpt/3999/params
 LAB_CORR2_STEPS=1500
 LAB_CORR2_INIT=/workspace/ckpt/corr_v1_1499/params
+LAB_CORR3_STEPS=1500
+LAB_CORR3_INIT=/workspace/ckpt/corr_v2_1499/params
 lab_model_def() {           # lab_model_def trim|corr → MODEL_REPO MODEL_CKPT MODEL_CONFIG MODEL_DATA MODEL_DESC. 知らない名前なら 1
     case "$1" in
         trim)
@@ -300,24 +303,31 @@ lab_model_def() {           # lab_model_def trim|corr → MODEL_REPO MODEL_CKPT
             MODEL_CONFIG=pi05_mecharm_trim_corr_v4_lora
             MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v4b     # 依頼 58: 182545 を外した 7 本 (8 本の corr_v4 は使わない)
             MODEL_DESC="corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (依頼 57 / 58)" ;;
+        corr3)
+            MODEL_REPO=PixelstoMotors/pi05-mecharm-trim-corr-lora-v3
+            MODEL_CKPT=/workspace/ckpt/corr_v3_1499
+            MODEL_CONFIG=pi05_mecharm_trim_corr_v5_lora
+            MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v5      # 依頼 97: corr_v4b の 7 本 + 11-06 のゲームパッドの 4 本
+            MODEL_DESC="corr2 の 1499 から trim2_corr_v5 で続きを学習した 1499 (依頼 97)" ;;
         *) return 1 ;;
     esac
 }
 
-lab_cmd_addon_model() {     # lab_cmd_addon_model trim|corr|corr2 → 3f (trim は lab_cmd_addon と同じ. corr・corr2 は学習の設定と assets も足す)
+lab_cmd_addon_model() {     # lab_cmd_addon_model trim|corr|corr2|corr3 → 3f (trim は lab_cmd_addon と同じ. corr〜corr3 は学習の設定と assets も足す)
     lab_model_def "$1" || return 1
     if [ "$1" = trim ]; then lab_cmd_addon; return; fi
     local corr_args="--steps $LAB_CORR_STEPS --init $LAB_CORR_INIT"
     [ "$1" = corr2 ] && corr_args="--variant v4 --steps $LAB_CORR2_STEPS --init $LAB_CORR2_INIT"
+    [ "$1" = corr3 ] && corr_args="--variant v5 --steps $LAB_CORR3_STEPS --init $LAB_CORR3_INIT"
     # assets は hf download が置いたモデルの assets/local/<データ名> を, openpi の assets/<設定>/local/ に写す
     # (trim の 3f が assets/pi05_mecharm_trim_lora/local/ に写すのと同じ形. 中身は trim2 と同じ正規化の値)
     echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/add_trim_corr_config.py /workspace/openpi $corr_args && mkdir -p assets/$MODEL_CONFIG/local && cp -r $MODEL_CKPT/assets/local/$MODEL_DATA assets/$MODEL_CONFIG/local/"
 }
 
-# ---- lab_train.sh で作るモデル (依頼 57. --model corr | corr2) ----
+# ---- lab_train.sh で作るモデル (依頼 57. --model corr | corr2. 依頼 97 で corr3) ----
 # 作るモデルの名前は lab_start.sh の --model と同じ. 続きの元 (TRAIN_FROM) も lab_model_def のモデル
-LAB_TRAIN_MODELS="corr corr2"
-lab_train_def() {           # lab_train_def corr|corr2 → TRAIN_* (知らない名前なら 1)
+LAB_TRAIN_MODELS="corr corr2 corr3"
+lab_train_def() {           # lab_train_def corr|corr2|corr3 → TRAIN_* (知らない名前なら 1)
     case "$1" in
         corr)                   # 依頼 49: 3999 から trim2_corr_v3 で続きを学習した 1499
             TRAIN_FROM=trim; TRAIN_VARIANT=v3
@@ -325,6 +335,9 @@ lab_train_def() {           # lab_train_def corr|corr2 → TRAIN_* (知らない
         corr2)                  # 依頼 57: corr の 1499 から trim2_corr_v4b (依頼 58) で続きを学習する
             TRAIN_FROM=corr; TRAIN_VARIANT=v4
             TRAIN_FROM_KIOXIA=pi05_mecharm_trim_corr_lora_v1/1499 ;;
+        corr3)                  # 依頼 97: corr2 の 1499 から trim2_corr_v5 で続きを学習する
+            TRAIN_FROM=corr2; TRAIN_VARIANT=v5
+            TRAIN_FROM_KIOXIA=pi05_mecharm_trim_corr_lora_v2/1499 ;;
         *) return 1 ;;
     esac
     lab_model_def "$1" || return 1
@@ -336,8 +349,8 @@ lab_train_def() {           # lab_train_def corr|corr2 → TRAIN_* (知らない
     lab_model_def "$1"          # MODEL_* は作るモデルのものに戻す
 }
 
-lab_cmd_addon_train() {     # lab_cmd_addon_train corr|corr2 → lab_train.sh の 3f
-    # corr は lab_cmd_addon と同じ (3999 を取っているので trim の assets も写す). corr2 は 3999 を取らないので
+lab_cmd_addon_train() {     # lab_cmd_addon_train corr|corr2|corr3 → lab_train.sh の 3f
+    # corr は lab_cmd_addon と同じ (3999 を取っているので trim の assets も写す). corr2・corr3 は 3999 を取らないので
     # addon と trim2 の設定だけを入れる (学習の設定と正規化の値は pod_prep_trim_corr.sh が置く)
     if [ "$1" = corr ]; then lab_cmd_addon; return; fi
     echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi"
diff --git a/lab_start.sh b/lab_start.sh
index bc67b77..41ec59b 100755
--- a/lab_start.sh
+++ b/lab_start.sh
@@ -13,8 +13,11 @@
 #              corr = 3999 から trim2_corr_v3 で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v1)
 #              corr2 = corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2.
 #                      依頼 57. lab_train.sh --model corr2 で作る. Pod の /workspace/ckpt/corr_v2_1499)
+#              corr3 = corr2 の 1499 から trim2_corr_v5 で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3.
+#                      依頼 97. lab_train.sh --model corr3 で作る. Pod の /workspace/ckpt/corr_v3_1499)
 #              取る場所・設定・assets・サーバーの起動・つなぎ直しの確かめが選んだモデルに合う. 安全範囲 (学習データ ±10°)
-#              と位置合わせのお手本はどれも trim2 の raw_episodes (corr_v3・corr_v4 と合わせても範囲はほぼ同じで, trim2 の方が狭い)
+#              と位置合わせのお手本はどれも trim2 の raw_episodes (corr_v3・corr_v4 と合わせても範囲はほぼ同じで, trim2 の方が狭い.
+#              corr_v5 は 11-06 の 184650 の j2 が trim2 の上限 +31.4° を最大 11° 超える)
 #              つなぎ直しで, 残っている Pod のサーバーが別のモデルで動いていれば, そのことを出して止まる
 #
 # 1. Pod を作る (A40×1・Runpod Pytorch 2.8.0・コンテナのディスク 200GB・22/tcp を外へ出す)
@@ -62,7 +65,7 @@ while [ $# -gt 0 ]; do
         --dry-run) DRY_RUN=1; shift ;;
         --model) [ $# -ge 2 ] || { echo "[ERROR] --model には ${LAB_MODELS// / か } を付ける" >&2; exit 2; }     # 値がないと shift 2 が失敗して回り続ける (codex R1)
                  MODEL="$2"; shift 2 ;;
-        -h|--help) sed -n '2,39p' "$0"; exit 0 ;;
+        -h|--help) sed -n '2,42p' "$0"; exit 0 ;;
         *) echo "[ERROR] 知らない引数: $1  (./lab_start.sh --help)" >&2; exit 2 ;;
     esac
 done
diff --git a/lab_train.sh b/lab_train.sh
index a42bc4b..5e5efe3 100755
--- a/lab_train.sh
+++ b/lab_train.sh
@@ -13,8 +13,11 @@
 #                    corr  = 3999 から trim2_corr_v3 で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v1
 #                    corr2 = corr の 1499 から trim2_corr_v4b で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v2
 #                            (設定 pi05_mecharm_trim_corr_v4_lora. 3c で取るのは 3999 ではなく corr の 1499)
-#   --steps / --init / --exp-name   学習 (既定 1500 / 続きの元の params / trim_corr_v3 か trim_corr_v4)
-#                    続きの元: corr は /workspace/ckpt/3999/params, corr2 は /workspace/ckpt/corr_v1_1499/params
+#                    corr3 = corr2 の 1499 から trim2_corr_v5 で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v3
+#                            (依頼 97. 設定 pi05_mecharm_trim_corr_v5_lora. 3c で取るのは corr2 の 1499)
+#   --steps / --init / --exp-name   学習 (既定 1500 / 続きの元の params / trim_corr_v3・trim_corr_v4・trim_corr_v5)
+#                    続きの元: corr は /workspace/ckpt/3999/params, corr2 は /workspace/ckpt/corr_v1_1499/params,
+#                              corr3 は /workspace/ckpt/corr_v2_1499/params
 #   --poll-min     進みを見る間 (既定 5 分)
 #   --stall-min    学習のログが何分書かれなければ「止まった」とするか (既定 30 分)
 #
@@ -62,7 +65,7 @@ while [ $# -gt 0 ]; do
         --stall-min) lab_need_val 30 "$@"; STALL_MIN="$2"; shift 2 ;;
         --stop) MODE=stop; shift ;;
         --dry-run) DRY_RUN=1; shift ;;
-        -h|--help) sed -n '2,36p' "$0"; exit 0 ;;
+        -h|--help) sed -n '2,39p' "$0"; exit 0 ;;
         *) echo "[ERROR] 知らない引数: $1  (./lab_train.sh --help)" >&2; exit 2 ;;
     esac
 done
@@ -92,17 +95,17 @@ if [ $DRY_RUN = 1 ]; then
 fi
 CKPT_ROOT="$DATA_ROOT/checkpoints"
 ADD_TRIM="$DATA_ROOT/lab/Log11/pod/add_trim_config.py"
-use_model() {               # use_model corr|corr2 → 作るモデルで決まる値 (lab_lib.sh の lab_train_def. 依頼 57)
+use_model() {               # use_model corr|corr2|corr3 → 作るモデルで決まる値 (lab_lib.sh の lab_train_def. 依頼 57 / 97)
     lab_train_def "$1" || return 1
-    DATA_NAME=$TRAIN_DATA                                   # corr: ..._trim2_corr_v3 / corr2: ..._trim2_corr_v4b (依頼 58)
+    DATA_NAME=$TRAIN_DATA                                   # corr: ..._trim2_corr_v3 / corr2: ..._trim2_corr_v4b (依頼 58) / corr3: ..._trim2_corr_v5
     TAR="$DATA_ROOT/datasets/$DATA_NAME/${DATA_NAME}_raw_episodes.tar"
-    CONFIG=$TRAIN_CONFIG                                    # corr: pi05_mecharm_trim_corr_lora / corr2: pi05_mecharm_trim_corr_v4_lora
-    HF_REPO_IN=$TRAIN_REPO_IN                               # 続きの元 (corr: 3999 / corr2: corr の 1499. lab_start.sh と同じ)
+    CONFIG=$TRAIN_CONFIG                                    # corr: pi05_mecharm_trim_corr_lora / corr2: ..._v4_lora / corr3: ..._v5_lora
+    HF_REPO_IN=$TRAIN_REPO_IN                               # 続きの元 (corr: 3999 / corr2: corr の 1499 / corr3: corr2 の 1499. lab_start.sh と同じ)
     HF_REPO_OUT=$TRAIN_REPO_OUT                             # 上げる先 (非公開). 名前は checkpoints/ のフォルダ名に合わせる
     CKPT_NAME=$TRAIN_CKPT_NAME                              # KIOXIA の checkpoints/<これ>/<ステップ>/
     CKPT_IN=$TRAIN_CKPT_IN                                  # 3c で続きの元を置く場所
     CKPT_IN_KIOXIA="$CKPT_ROOT/$TRAIN_FROM_KIOXIA"          # HF_REPO_IN と同じもの (由来の指紋を取る. 依頼 54)
-    PREP_VARIANT=$TRAIN_VARIANT                             # pod_prep_trim_corr.sh の 3 つ目 (v3 / v4)
+    PREP_VARIANT=$TRAIN_VARIANT                             # pod_prep_trim_corr.sh の 3 つ目 (v3 / v4 / v5)
 }
 use_model "$MODEL"
 POD_EXP_DIR=/workspace/openpi/checkpoints/$CONFIG/$EXP
@@ -154,7 +157,7 @@ hm() { local x=$1; [ "$x" -lt 0 ] && x=0; printf '%d:%02d' $((x / 3600)) $((x %
 PROV_PROCESS=""; PROV_ERROR=""; PROV_CKPT_DIR=""; PROV_HF=0; PROV_DONE=0
 OPENPI_JSON='[]'            # 3f のあとに Pod の openpi のコミットと変更の有無を入れる
 iso_of() { [ -n "${1:-}" ] && [ "$1" != 0 ] && date -r "$1" '+%Y-%m-%dT%H:%M:%S%z' | sed -E 's/([0-9]{2})$/:\1/'; }
-prov_inputs() {             # 入力 (学習データ・続きの元 (corr は 3999・corr2 は corr の 1499)・設定を足すスクリプト) を spec の inputs の形で出す
+prov_inputs() {             # 入力 (学習データ・続きの元 (corr は 3999・corr2 は corr の 1499・corr3 は corr2 の 1499)・設定を足すスクリプト) を spec の inputs の形で出す
     local init_ck=""
     case "$INIT" in "$CKPT_IN"/*) init_ck="$CKPT_IN_KIOXIA" ;; esac
     jq -n --arg data "$DATA_NAME" --arg data_dir "$DATA_ROOT/datasets/$DATA_NAME" \
diff --git a/lab_train_fakes.sh b/lab_train_fakes.sh
index 2f787e7..87a5d9f 100644
--- a/lab_train_fakes.sh
+++ b/lab_train_fakes.sh
@@ -36,6 +36,7 @@ echo "[DRY   ] ドライラン: Pod は借りません. 書くのは $LAB_DRY_RO
 if [ -d /Volumes/KIOXIA/r2r2r-data ]; then
     for f in "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v3/mecharm_redblock_box_teach_v1_trim2_corr_v3_raw_episodes.tar" \
              "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v4b/mecharm_redblock_box_teach_v1_trim2_corr_v4b_raw_episodes.tar" \
+             "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5/mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar" \
              /Volumes/KIOXIA/r2r2r-data/lab/Log11/pod/add_trim_config.py "$RUNPOD_KEY_FILE" "$HF_TOKEN_FILE"; do
         echo "[DRY   ] 本物の $(basename "$f"): $([ -s "$f" ] && echo ある || echo ない)"
     done
@@ -46,13 +47,14 @@ LAB_ROOT="$DATA_ROOT/lab"; LAB_CURRENT="$LAB_ROOT/.lab_current"
 RUNPOD_KEY_FILE="$LAB_DRY_ROOT/keys/runpod_api_key.txt"; HF_TOKEN_FILE="$LAB_DRY_ROOT/keys/hf_token.txt"
 POD_SSH_KEY="$LAB_DRY_ROOT/keys/id_ed25519"
 mkdir -p "$LAB_DRY_ROOT/keys" "$LAB_DRY_ROOT/fake" "$DATA_ROOT/lab/Log11/pod" \
-         "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v3" "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v4b"
+         "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v3" "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v4b" \
+         "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5"
 [ -s "$RUNPOD_KEY_FILE" ] || echo "DRY-RUNPOD-SECRET-KEY" > "$RUNPOD_KEY_FILE"
 [ -s "$HF_TOKEN_FILE" ] || echo "hf_DRYSECRETTOKEN" > "$HF_TOKEN_FILE"
 [ -s "$POD_SSH_KEY" ] || echo "dry-ssh-key" > "$POD_SSH_KEY"
 [ -s "$POD_SSH_KEY.pub" ] || echo "ssh-ed25519 AAAA dry" > "$POD_SSH_KEY.pub"
 [ -f "$DATA_ROOT/lab/Log11/pod/add_trim_config.py" ] || echo "# dry" > "$DATA_ROOT/lab/Log11/pod/add_trim_config.py"
-for v in v3 v4b; do         # --model corr は corr_v3, corr2 は corr_v4b (依頼 57 / 58)
+for v in v3 v4b v5; do      # --model corr は corr_v3, corr2 は corr_v4b (依頼 57 / 58), corr3 は corr_v5 (依頼 97)
     TARF="$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_$v/mecharm_redblock_box_teach_v1_trim2_corr_${v}_raw_episodes.tar"
     [ -f "$TARF" ] || echo "dry tar $v" > "$TARF"
 done
diff --git a/pi05/openpi_addon/add_trim_corr_config.py b/pi05/openpi_addon/add_trim_corr_config.py
index d928407..2dd76a0 100644
--- a/pi05/openpi_addon/add_trim_corr_config.py
+++ b/pi05/openpi_addon/add_trim_corr_config.py
@@ -29,6 +29,10 @@ pi05_mecharm_trim_corr_v4_lora を足す (名前が違うので v3 の設定と
   python3 add_trim_corr_config.py /workspace/openpi --variant v4 --steps 1500 --init /workspace/ckpt/corr_v1_1499/params
 依頼 58: v4 のデータを ..._trim2_corr_v4b (corr_v4 から 10-31 の 182545 を外した 7 本) に替えた. 設定の名前は
 pi05_mecharm_trim_corr_v4_lora のまま. 8 本の ..._trim2_corr_v4 の設定が入っている config.py には足さずに止まる
+依頼 97: --variant v5 で, corr_v5 (corr_v4b の 7 本 + 11-06 のゲームパッドで直した 4 本) と合わせた ..._trim2_corr_v5 の設定
+pi05_mecharm_trim_corr_v5_lora を足す (v3・v4 と並べて入る). 学習率の予定・ほかの値は v3 と同じ.
+続きの元 (--init) は corr_v2_1499 (/workspace/ckpt/corr_v2_1499/params)
+  python3 add_trim_corr_config.py /workspace/openpi --variant v5 --steps 1500 --init /workspace/ckpt/corr_v2_1499/params
 """
 import argparse
 import re
@@ -41,6 +45,7 @@ VARIANTS = {
            "old_tags": ("teach_v1_trim2_corr_v1", "teach_v1_trim2_corr_v2")},   # 依頼 45 / 46 で入れた版
     "v4": {"name": "pi05_mecharm_trim_corr_v4_lora", "data_tag": "teach_v1_trim2_corr_v4b",
            "old_tags": ("teach_v1_trim2_corr_v4",)},   # 依頼 57 / 58 (8 本の corr_v4 → 182545 を外した 7 本の corr_v4b)
+    "v5": {"name": "pi05_mecharm_trim_corr_v5_lora", "data_tag": "teach_v1_trim2_corr_v5", "old_tags": ()},  # 依頼 97
 }
 
 
diff --git a/pi05/openpi_addon/pod_prep_trim_corr.sh b/pi05/openpi_addon/pod_prep_trim_corr.sh
index 7296957..575f190 100644
--- a/pi05/openpi_addon/pod_prep_trim_corr.sh
+++ b/pi05/openpi_addon/pod_prep_trim_corr.sh
@@ -6,13 +6,17 @@
 #         /workspace/data/mecharm_redblock_box_teach_v1_trim2_corr_v3_raw_episodes.tar
 #         /workspace/r2r2r/pi05/  (この依頼の版. add_trim_corr_config.py と convert_to_lerobot.py を使う)
 #
-#   bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh <steps> <init> [v3|v4]
+#   bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh <steps> <init> [v3|v4|v5]
 #   例: bash ... 1500 /workspace/ckpt/3999/params
 #   依頼 57: 3 つ目に v4 を付けると trim2 + corr_v4 (..._trim2_corr_v4・設定 pi05_mecharm_trim_corr_v4_lora) の準備.
 #     例: bash ... 1500 /workspace/ckpt/corr_v1_1499/params v4
 #     続きの元は corr_v1_1499 (lab_train.sh --model corr2 の 3c が取る). 3999 は取らないので, 正規化の値は
 #     1499 の assets にある値 (3999 の trim2 の値と sha256 が同じ. 依頼 52 で確かめた) を, 同じ sha256 で確かめて置く
 #   依頼 58: v4 のデータは ..._trim2_corr_v4b (8 本の corr_v4 から 182545 を外した 7 本. 合わせて 26 本). 設定の名前は同じ
+#   依頼 97: v5 は trim2 + corr_v5 (..._trim2_corr_v5・設定 pi05_mecharm_trim_corr_v5_lora. 合わせて 30 本).
+#     続きの元は corr_v2_1499 (lab_train.sh --model corr3 の 3c が取る). 正規化の値は 1499 の assets にある値
+#     (trim2 の値と sha256 が同じ. 依頼 97 で KIOXIA の写しを確かめた) を, 同じ sha256 で確かめて置く
+#     例: bash ... 1500 /workspace/ckpt/corr_v2_1499/params v5
 #   v3 (既定) は今までと同じ
 #
 # すること (何度流しても同じ結果):
@@ -41,13 +45,17 @@ case "$VARIANT" in
         CONFIG=pi05_mecharm_trim_corr_v4_lora
         NORM_SRC=/workspace/ckpt/corr_v1_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v3/norm_stats.json
         EXP=trim_corr_v4; N_MAC=26 ;;
-    *) echo "[ERROR] 3 つ目は v3 か v4: $VARIANT" >&2; exit 2 ;;
+    v5) NAME=mecharm_redblock_box_teach_v1_trim2_corr_v5
+        CONFIG=pi05_mecharm_trim_corr_v5_lora
+        NORM_SRC=/workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json
+        EXP=trim_corr_v5; N_MAC=30 ;;
+    *) echo "[ERROR] 3 つ目は v3 か v4 か v5: $VARIANT" >&2; exit 2 ;;
 esac
 TAR=/workspace/data/${NAME}_raw_episodes.tar
 DATA=/workspace/data/$NAME
 OPENPI=/workspace/openpi
 ADDON=/workspace/r2r2r/pi05/openpi_addon
-NORM_SHA256=80346837eb12ffa6e683416f0fa41a196519fa8459d1f0b666b06cd621c7f61b   # Mac の写しの sha256 (v3 も v4 も同じ trim2 の値)
+NORM_SHA256=80346837eb12ffa6e683416f0fa41a196519fa8459d1f0b666b06cd621c7f61b   # Mac の写しの sha256 (v3・v4・v5 とも同じ trim2 の値)
 NORM_DST=assets/$CONFIG/local/$NAME/norm_stats.json                           # $OPENPI からの相対
 
 [ -f "$TAR" ] || { echo "[ERROR] tar がない: $TAR" >&2; exit 1; }
diff --git a/pi05/test_merge_and_config.py b/pi05/test_merge_and_config.py
index 4239e75..eb4d17d 100644
--- a/pi05/test_merge_and_config.py
+++ b/pi05/test_merge_and_config.py
@@ -15,6 +15,8 @@
     pod_prep_trim_corr.sh の v4 / KIOXIA の corr_v4 が決めた 8 本で, 前の 5 本は corr_v3 と同じ
   - 依頼 58: v4 のデータは ..._trim2_corr_v4b (corr_v4 から 182545 を外した 7 本) / 8 本の corr_v4 の設定が入っていたら止まる /
     KIOXIA の corr_v4b は決めた 7 本で, 中身は corr_v4 と同じ. 8 本の corr_v4 は残っている
+  - 依頼 97: --variant v5 (pi05_mecharm_trim_corr_v5_lora・..._trim2_corr_v5・続きの元 corr_v2_1499) が v3・v4 と並べて入る /
+    pod_prep_trim_corr.sh の v5 / KIOXIA の corr_v5 は corr_v4b の 7 本 (中身も同じ) と 11-06 の 4 本
 """
 import ast
 import json
@@ -297,6 +299,40 @@ class TestConfigV4(unittest.TestCase):
             self.assertEqual(cfg.read_text(), before)
 
 
+class TestConfigV5(unittest.TestCase):
+    """依頼 97: --variant v5 は別の名前の設定として v3・v4 と並べて入り, データと続きの元だけが違う."""
+    INIT5 = "/workspace/ckpt/corr_v2_1499/params"
+    INIT4 = "/workspace/ckpt/corr_v1_1499/params"
+
+    def test_v5_with_v3_v4(self):
+        for order in (("v5",), ("v3", "v4", "v5"), ("v5", "v4")):
+            with tempfile.TemporaryDirectory() as td, mock.patch("sys.stdout"):
+                cfg = fake_config(Path(td))
+                inits = {"v3": "/workspace/ckpt/3999/params", "v4": self.INIT4, "v5": self.INIT5}
+                for v in order:
+                    self.assertEqual(atc.main([td, "--variant", v, "--steps", "1500", "--init", inits[v]]), 0)
+                c = configs(cfg)
+                self.assertIn("pi05_mecharm_trim_corr_v5_lora", c)
+                v5 = c["pi05_mecharm_trim_corr_v5_lora"]
+                self.assertIn("'local/mecharm_redblock_box_teach_v1_trim2_corr_v5'", v5["data"])
+                self.assertIn(self.INIT5, v5["weight_loader"])
+                self.assertEqual(v5["num_train_steps"], "1500")
+                lr = {k.arg: ast.literal_eval(k.value) for k in ast.parse(v5["lr_schedule"]).body[0].value.keywords}
+                self.assertEqual(lr, {"warmup_steps": 100, "peak_lr": 2.5e-5, "decay_steps": 1500, "decay_lr": 5e-6})
+                if "v4" in order:      # v4 は今までのまま (corr_v4b・corr_v1_1499). データ・続きの元のほかは同じ値
+                    v4 = c["pi05_mecharm_trim_corr_v4_lora"]
+                    self.assertIn("'local/mecharm_redblock_box_teach_v1_trim2_corr_v4b'", v4["data"])
+                    self.assertIn(self.INIT4, v4["weight_loader"])
+                    for k in set(v4) - {"name", "data", "weight_loader"}:
+                        self.assertEqual(v5[k], v4[k], k)
+                text = cfg.read_text()
+                self.assertEqual(atc.main([td, "--variant", "v5", "--steps", "1500", "--init", self.INIT5]), 0)
+                self.assertEqual(cfg.read_text(), text)                    # 二重に入れない
+                with self.assertRaises(SystemExit):                        # 違う続きの元では止まる
+                    atc.main([td, "--variant", "v5", "--steps", "1500", "--init", self.INIT4])
+                self.assertEqual(cfg.read_text(), text)
+
+
 class TestPodPrep(unittest.TestCase):
     """依頼 48: pod_prep_trim_corr.sh は正規化の値を計算し直さず, 3999 の学習の trim2 の値を置く."""
     SH = _HERE / "openpi_addon" / "pod_prep_trim_corr.sh"
@@ -344,6 +380,21 @@ class TestPodPrep(unittest.TestCase):
             m = re.search(r"^NORM_SHA256=([0-9a-f]{64})", self.SH.read_text(), re.M)
             self.assertEqual(hashlib.sha256(mac.read_bytes()).hexdigest(), m.group(1))
 
+    def test_v5_uses_corr2_1499_norm(self):
+        """依頼 97: v5 は corr_v5 の設定で, 正規化の値は続きの元 (corr2 の 1499) の assets から同じ sha256 で確かめて置く."""
+        code = "\n".join(self.lines())
+        for x in ("NAME=mecharm_redblock_box_teach_v1_trim2_corr_v5\n", "N_MAC=30 ;;", "CONFIG=pi05_mecharm_trim_corr_v5_lora",
+                  "NORM_SRC=/workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json",
+                  "EXP=trim_corr_v5;"):
+            self.assertIn(x, code)
+        mac = Path("/Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v2/1499/assets/local/"
+                   "mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json")
+        if mac.exists():
+            import hashlib
+            import re
+            m = re.search(r"^NORM_SHA256=([0-9a-f]{64})", self.SH.read_text(), re.M)
+            self.assertEqual(hashlib.sha256(mac.read_bytes()).hexdigest(), m.group(1))
+
     def test_unknown_variant_stops_before_anything(self):
         r = subprocess.run(["bash", str(self.SH), "1500", "base", "v9"], capture_output=True, text=True)
         self.assertEqual(r.returncode, 2)
@@ -413,5 +464,36 @@ class TestCorrV4bData(unittest.TestCase):
         self.assertEqual(sorted(Path(n).parent.name for n in names if n.endswith("/episode.npz")), eps)
 
 
+@unittest.skipUnless((DS / "mecharm_redblock_box_corr_v5").is_dir(), "KIOXIA の corr_v5 がない")
+class TestCorrV5Data(unittest.TestCase):
+    """依頼 97: corr_v5 は corr_v4b の 7 本 (中身も同じ) と, 11-06 のゲームパッドで直した成功のうち外れのない 4 本.
+    corr_v4b と合わせたもの (v4b) は残す."""
+    OLD = TestCorrV4bData.RUNS
+    NEW = ["pi05_20261106_183926", "pi05_20261106_184452", "pi05_20261106_184650", "pi05_20261106_184906"]
+
+    def test_runs_and_old_same(self):
+        raw = DS / "mecharm_redblock_box_corr_v5" / "raw_episodes"
+        self.assertEqual(sorted(p.parent.name for p in raw.glob("[!.]*/episode.npz")), sorted(self.OLD + self.NEW))
+        for r in self.OLD:
+            a = (DS / "mecharm_redblock_box_corr_v4b" / "raw_episodes" / r / "episode.npz").read_bytes()
+            self.assertEqual(a, (raw / r / "episode.npz").read_bytes(), r)
+        for r in self.NEW:
+            m = json.loads((raw / r / "meta.json").read_text())
+            self.assertEqual(m["gap_fill"]["mode"], "speed20")
+            self.assertEqual(m["trim_hold"], {"keep_model": 0, "keep_correct": 1, "keep_grip": 2})
+            self.assertGreater(m["correct_steps"], 0)
+        self.assertNotIn("pi05_20261106_185554", [p.parent.name for p in raw.glob("*/episode.npz")])
+        self.assertEqual(len(list((DS / "mecharm_redblock_box_corr_v4b" / "raw_episodes").glob("[!.]*/episode.npz"))), 7)
+
+    def test_merged_and_tar(self):
+        ds = DS / "mecharm_redblock_box_teach_v1_trim2_corr_v5"
+        eps = sorted(p.parent.name for p in (ds / "raw_episodes").glob("[!.]*/episode.npz"))
+        self.assertEqual(len(eps), 30)
+        tar = ds / "mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar"
+        names = subprocess.run(["tar", "-tf", str(tar)], capture_output=True, text=True, check=True).stdout.split()
+        self.assertFalse([n for n in names if Path(n).name.startswith("._")])
+        self.assertEqual(sorted(Path(n).parent.name for n in names if n.endswith("/episode.npz")), eps)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/test_lab_start.sh b/test_lab_start.sh
index be6f152..c481953 100755
--- a/test_lab_start.sh
+++ b/test_lab_start.sh
@@ -398,13 +398,27 @@ check "corr2 で動いている Pod に --model corr2 → 3b〜4 を飛ばして
 rm -rf "$T"
 check "フォルダからモデルの名前 (corr2)" '[ "$(. "$HERE/lab_lib.sh"; lab_model_of_dir /workspace/ckpt/corr_v2_1499)" = corr2 ]'
 
+echo "== 34c. --model corr3 (依頼 97) で新しく作る → corr_v5 で学習したモデルを取り, v5 の設定・assets を足して起動"
+setup; run --model corr3
+check "終了コード 0" '[ $RC = 0 ]'
+check "corr3 のモデルを /workspace/ckpt/corr_v3_1499 に取る (ほかのモデルは取らない)" 'has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 --local-dir /workspace/ckpt/corr_v3_1499" && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-lora-v1 " && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 "'
+check "3f で学習と同じ v5 の設定 (続きの元 corr_v2_1499) を足し, モデルの assets を写す" 'has ssh.log "add_trim_corr_config.py /workspace/openpi --variant v5 --steps 1500 --init /workspace/ckpt/corr_v2_1499/params" && has ssh.log "mkdir -p assets/pi05_mecharm_trim_corr_v5_lora/local && cp -r /workspace/ckpt/corr_v3_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v5 assets/pi05_mecharm_trim_corr_v5_lora/local/"'
+check "corr3 の設定とフォルダでサーバーを起動" 'has ssh.log "--policy.config=pi05_mecharm_trim_corr_v5_lora --policy.dir=/workspace/ckpt/corr_v3_1499 2>&1"'
+check "クライアントに --model corr3 を渡す・安全範囲と位置合わせは trim2 のまま" 'has py.log "--model corr3 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3)" && has py.log "datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes "'
+check "記録に model=corr3" '[ "$(jq -r .model "$T/data/lab/LogT/pod/lab_pod.json")" = corr3 ]'
+rm -rf "$T"
+setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1; serve_dir /workspace/ckpt/corr_v2_1499; run --model corr3
+check "corr2 のサーバーが動いている Pod に --model corr3 → 止まり, --model corr2 を案内" '[ $RC = 1 ] && has out.txt "別のモデル (--model corr2)で動いています" && ! has ssh.log "tmux new" && ! has ssh.log "pkill"'
+rm -rf "$T"
+check "フォルダからモデルの名前 (corr3)" '[ "$(. "$HERE/lab_lib.sh"; lab_model_of_dir /workspace/ckpt/corr_v3_1499)" = corr3 ]'
+
 echo "== 35. --model に知らない名前 → 何もせずに止まる"
 setup; run --model foo
-check "終了コード 2・使える名前を出す・API を呼ばない" '[ $RC = 2 ] && has out.txt "--model は trim か corr か corr2" && [ ! -s "$T/calls.log" ]'
+check "終了コード 2・使える名前を出す・API を呼ばない" '[ $RC = 2 ] && has out.txt "--model は trim か corr か corr2 か corr3" && [ ! -s "$T/calls.log" ]'
 rm -rf "$T"
 setup
 (cd "$T" && PATH="$T/bin:$PATH" perl -e 'alarm 10; exec @ARGV' ./lab_start.sh --log LogT --dry-run --model < /dev/null > "$T/out.txt" 2>&1); RC=$?
-check "--model だけで値がない → 回り続けずに終了コード 2 (codex R1)" '[ $RC = 2 ] && has out.txt "--model には trim か corr か corr2 を付ける"'
+check "--model だけで値がない → 回り続けずに終了コード 2 (codex R1)" '[ $RC = 2 ] && has out.txt "--model には trim か corr か corr2 か corr3 を付ける"'
 rm -rf "$T"
 for a in "--gpu" "--episode" "--log" "--gpu --dry-run" "--episode --log LogT" "--log --dry-run"; do
     setup
diff --git a/test_lab_train.sh b/test_lab_train.sh
index fafe2cf..4bf9c55 100755
--- a/test_lab_train.sh
+++ b/test_lab_train.sh
@@ -213,6 +213,28 @@ TD="$T/data/lab/LogT/train"
 check "--stop (--model なし): 記録の model=corr2 の設定・exp でログと一覧を取る" '[ $(n_call DELETE) = 1 ] && [ -f "$TD/train_trim_corr_v4.log" ] && has ssh.log "cd /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v4_lora/trim_corr_v4" && [ "$(jq -r .config.data_name "$TD/train_provenance.json")" = mecharm_redblock_box_teach_v1_trim2_corr_v4b ]'
 rm -rf "$T"
 
+echo "== 12c. --model corr3 (依頼 97): corr2 の 1499 から trim2_corr_v5 で続きを学習する"
+run success --model corr3
+CK3="$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v3"
+check "終了コード 0・画面に --model corr3 と続きの元 → 上げる先" '[ $RC = 0 ] && has out.txt "--model corr3 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v3)"'
+check "3c は corr2 の 1499 を /workspace/ckpt/corr_v2_1499 に取る (3999・corr の 1499 は取らない)" 'has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 --local-dir /workspace/ckpt/corr_v2_1499" && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-lora-v1 " && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v1 "'
+check "3f は 3999 の assets を写さない" 'has ssh.log "add_trim_config.py /workspace/openpi" && ! has ssh.log "cp -r /workspace/ckpt/3999/"'
+check "corr_v5 の tar を送り, pod_prep_trim_corr.sh 1500 /workspace/ckpt/corr_v2_1499/params v5" 'has scp.log "mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar root@" && ! has scp.log "corr_v4b_raw_episodes.tar" && ! has scp.log "corr_v3_raw_episodes.tar" && has ssh.log "pod_prep_trim_corr.sh 1500 /workspace/ckpt/corr_v2_1499/params v5"'
+check "学習は pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5" 'has ssh.log "uv run scripts/train.py pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5 --overwrite > /workspace/logs/train_trim_corr_v5.log"'
+check "Hugging Face の pi05-mecharm-trim-corr-lora-v3 へ上げ, KIOXIA の checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 に置く" 'has ssh.log "hf_upload_ckpt.py --dir /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499 --repo PixelstoMotors/pi05-mecharm-trim-corr-lora-v3" && [ -f "$CK3/1499/_CHECKPOINT_METADATA" ] && [ ! -e "$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v2/1499/assets" ]'
+check "記録 lab_pod.json に model・設定・exp・init" '[ "$(jq -r "[.model,.config,.exp,.init] | join(\" \")" "$TD/lab_pod.json")" = "corr3 pi05_mecharm_trim_corr_v5_lora trim_corr_v5 /workspace/ckpt/corr_v2_1499/params" ]'
+P3="$TD/train_provenance.json"
+check "由来: 入力は corr_v5 と corr2 の 1499 (KIOXIA の原本・指紋・Hugging Face の元)" '[ "$(jq -r ".inputs[0].id" "$P3")" = mecharm_redblock_box_teach_v1_trim2_corr_v5 ] && [ "$(jq -r ".inputs[1] | [.kind,.id,.path,.hf_repo,.pod_path] | join(\" \")" "$P3")" = "checkpoint pi05_mecharm_trim_corr_lora_v2/1499 checkpoints/pi05_mecharm_trim_corr_lora_v2/1499 PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 /workspace/ckpt/corr_v2_1499/params" ] && [ "$(jq -r ".inputs[1].fingerprint" "$P3")" = "$(python3 "$HERE/provenance.py" fingerprint "$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v2/1499")" ]'
+check "由来: 設定・出力・チェックポイント側と Hugging Face にも同じもの" '[ "$(jq -r ".config | [.name,.exp_name,.init_params,.data_name] | join(\" \")" "$P3")" = "pi05_mecharm_trim_corr_v5_lora trim_corr_v5 /workspace/ckpt/corr_v2_1499/params mecharm_redblock_box_teach_v1_trim2_corr_v5" ] && [ "$(jq -r ".outputs[0] | [.path,.hf_repo] | join(\" \")" "$P3")" = "checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 PixelstoMotors/pi05-mecharm-trim-corr-lora-v3" ] && cmp -s "$P3" "$CK3/1499/train_provenance.json" && has calls.log "hf upload PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 train_provenance.json"'
+rm -rf "$T"
+T=$(mktemp -d); mkdir -p "$T/data/lab/LogT/train" "$T/fake"
+jq -n '{id:"dryrunpod0001", kind:"train", model:"corr3", config:"pi05_mecharm_trim_corr_v5_lora", gpu:"NVIDIA A40", costPerHr:0.44, created_epoch:1793449178, created_at:"2026-10-31 21:19:38", exp:"trim_corr_v5", train_started_epoch:1793449300}' > "$T/data/lab/LogT/train/lab_pod.json"
+echo "NVIDIA A40" > "$T/fake/created"
+(cd "$HERE" && LAB_DRY_ROOT="$T" ./lab_train.sh --log LogT --dry-run --stop < /dev/null > "$T/out.txt" 2>&1); RC=$?
+TD="$T/data/lab/LogT/train"
+check "--stop (--model なし): 記録の model=corr3 の設定・exp でログと一覧を取る" '[ $(n_call DELETE) = 1 ] && [ -f "$TD/train_trim_corr_v5.log" ] && has ssh.log "cd /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5" && [ "$(jq -r .config.data_name "$TD/train_provenance.json")" = mecharm_redblock_box_teach_v1_trim2_corr_v5 ]'
+rm -rf "$T"
+
 echo "== 13. 見守る途中で止められても Pod を残さない (TERM. Ctrl-C・HUP も同じ扱い)"
 T=$(mktemp -d)
 (cd "$HERE" && LAB_DRY_ROOT="$T" LAB_FAKE_SCENARIO=timeout LAB_DRY_POLL_SEC=1 ./lab_train.sh --log LogT --dry-run --limit-hours 1 < /dev/null > "$T/out.txt" 2>&1) &
```

### この回の commit

#### r2r2r-scripts の commit（11/5〜11/6）

```text
6dac235 11-06 20:08 依頼 97 の作業の記録
284dfd7 11-06 19:51 11-06 のゲームパッドで直した成功 5 本を diag_joint_hold.py で点検できるようにし (rpc 行 send_angle / stop を log.jsonl の pad の行と組にして指令を作る・長押しの関節は見ない・送ったらすぐ返る daemon の区間は向かう途中を遅れに数えず指令から離れた分 away を見る. 11-01 の結果は変わらない), 外れのない 183926・184452・184650・184906 を corr_v4b の 7 本に足した corr_v5 で, corr2 の 1499 から続きを学習する lab_train.sh / lab_start.sh の --model corr3 を足す (設定 pi05_mecharm_trim_corr_v5_lora・上げる先 pi05-mecharm-trim-corr-lora-v3). build_correction_episodes がゲームパッドの区間を扱えることを test_pad_build.py で確かめる (依頼 97. codex 3 回)
cd2fb46 11-05 19:42 run_policy_client: 推論のあとの確認に p (ゲームパッドで補正) を足す. gamepad_teleop を同じプロセスで動かし □ で推論に戻る (LOG450)
```
