# コードレビュー依頼：補正データの角度の作り直し（A）と、記録で daemon とのやりとりを残す（B）（2026-10-31 依頼 46）

前提: 依頼 45（コミット e29754e）の続き。依頼 45 で作った corr_v1 は、実測角がアームの動いている間ほとんど取れておらず（1.5〜3.6 秒の抜け）、10Hz の state が抜けの前後をまっすぐ結ぶ線になっていた。
過去のレビュー: docs/review_result_20261031_confirm_keys_flush_codex.md ほか

対象（`git diff HEAD -- <下のファイル>` と新しいファイル。ほかのファイルの未コミット差分は対象外）:
- `pi05/build_correction_episodes.py`（A: fill_motion_gaps / logged_sends / match_gaps_to_sends / --gap-fill / meta の correction_rows・gap_fill・max_step_deg）
- `pi05/policy_recorder.py`（B: install_daemon_lock の観察口 / PolicyRecorder._on_rpc / angles_ok）
- `pi05/run_policy_client.py`（B: PolicyRecorder に angles_ok=wr._is_valid_angles を渡す 1 行だけ）
- `pi05/openpi_addon/add_trim_corr_config.py`（データ名を corr_v2 に。corr_v1 の設定が入っていたら止まる）
- `pi05/openpi_addon/pod_prep_trim_corr.sh`（新。Pod で tar を広げ・設定を足し・変換・正規化の値の計算）
- テスト: `pi05/test_gap_fill.py`（新 6 件）、`pi05/test_recorder_rpc.py`（新 3 件・偽の daemon）、`pi05/test_merge_and_config.py`（9 件）、`pi05/test_rewind_correct.py`（69 件・変更なし。全部通る）

## 調べて分かったこと（B の原因）
- Pi の daemon（mecharm_daemon.py.pi_deployed_20261019_LOG352）は send_angles を受けると、送ったあとに `read_angles_with_retry`（最大 12 回 × 約 0.31 秒）で角度を読み直してから返事をする。動いている間は読めずにやり直すので、Log17 では 1 回の送信が約 1.6 秒（爪は daemon の 2 秒の待ちを足して約 3.6 秒）
- daemon は 1 本のつながりを 1 件ずつ処理し（listen(1)・serve_client を順に）、Mac 側も install_daemon_lock の鍵で 1 回ずつ使う。だから送信の間は記録スレッドも読めない
- Log17 の 180435 では、補正の 1 刻みの時刻（log.jsonl の t）の直後から抜けが始まり、抜けの終わりの実測角は目標にほぼ着いている。中身がほとんど動かない送信（0.1° 程度）でも 1.6 秒の抜けになる
- trim2 の元の記録（run_pick_place 19 本。送信の wait=1.0）では 0.32 秒おきに動いている途中も読めており、先頭の関節の速さは 128 区間の中央値 29.8°/秒（10〜90% で 28.7〜31.1）

## 変えたこと
A. `--gap-fill speed20`：実測角の間が 0.25 秒より長い「抜け」[ta, tb] だけ、ta から前後の実測角を結ぶ直線（関節空間・全関節が同時に着く）を、いちばん大きく動く関節が 30°/秒になる速さで進み、着いたら tb まで止まる。着くのに抜けより長くかかる動きは抜けいっぱい（linear と同じ）。実測角があるところは今までどおり。`--gap-fill linear`（既定）は corr_v1 と同じ結果（5 本で 1469 コマ・補正 50 コマ。同じになることを確かめた）。log.jsonl の送った目標（チャンクの sent_targets・補正の after）は、抜けと組にできるかと、抜けの終わりの実測角との差を meta に出すだけで、埋め方には使わない
B. 記録の側で、送る指令・到着の判定・待ち時間を変えずにできることだけ：
  - install_daemon_lock の差し替えた関数が、やりとりが終わって鍵を外したあとに、足された観察の関数へ (送った dict, 返事, 始めた時刻, 終わった時刻, 例外) を渡す。返事・例外はそのまま返す／投げ直す。観察の関数の例外は捨てる
  - PolicyRecorder は start で観察を足し stop で外す。get_angles は記録スレッド以外（本体の到着の確かめ・送る直前の読み）の読めた角度を actual 行（note=main）に残す。send_angles は send_angles 行（ts=ts_send=始めた時刻・目標・速度・note=rpc_sec）、ping 以外のほかの cmd は rpc 行
  - 抜けそのものは daemon の読み直しが原因なので、記録の側だけでは消えない（偽の daemon で確かめた。下の表）。daemon を変えるかはエイイチが決める

偽の daemon（1 件ずつ処理・送ったあと 1.6 秒の読み直し・腕は 30°/秒で動く）で、中継点 3 つを run_policy_client と同じ順で送ったとき：

| daemon の送ったあとの読み直し | 本体の読みも残す | 動いている間の最大の間隔 | 動いている間に残った数 |
| --- | --- | --- | --- |
| 1.6 秒 | しない（前） | 1.74 秒 | 0 |
| 1.6 秒 | する | 1.65 秒 | 0 |
| 0 秒（参考） | しない | 0.18 秒 | 8 |
| 0 秒（参考） | する | 0.11 秒 | 11 |

## 見てほしい点
1. B の観察口が、本体の動き（daemon に届く指令・タイムアウト・例外の型・再接続の get_daemon_socket → 再帰の ping）を変えていないか。鍵の外で観察を呼ぶ順序・RLock の再入（ping）で問題がないか
2. _on_rpc がスレッドの間で安全か（csv_lock・stop のあとに呼ばれたとき・記録スレッドの読みを二重に残さない判定）。states.csv に新しい行（send_angles / rpc / actual note=main）が入っても build_demo_episodes.load_states・build_correction_episodes・ほかの読み手が壊れないか
3. A の埋め方：抜けの頭で動き出すと見なすこと・30°/秒・0.25 秒の閾値・直線で埋めることの妥当性。抜けが 2 つの送信をまたぐとき、送信でない抜け（爪・記録の止まり）での振る舞い
4. match_gaps_to_sends の数え方（補正は送る直前の t、チャンクは実行の窓の中を順に組にする）が誤って組にしていないか（確かめのためだけで、データには使わない）
5. add_trim_corr_config.py の corr_v1 → corr_v2 の切り替えと止まる条件、pod_prep_trim_corr.sh の手順（set -euo pipefail・何度流してもよいか・convert_to_lerobot の rmtree の範囲）
6. テストが A/B を十分に確かめているか

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