Skip to content

fix: Cockpit の真っ黒タイル・別エージェントへの誤送信・端末の大きさ + CI を 5 分予算へ - #36

Merged
tacyan merged 4 commits into
mainfrom
fix/cockpit-blank-tiles-and-widget-id-shift
Aug 18, 2026
Merged

tacyan merged 4 commits into
mainfrom
fix/cockpit-blank-tiles-and-widget-id-shift

Conversation

@tacyan

@tacyan tacyan commented Aug 17, 2026 •

Copy link
Copy Markdown
Owner

利用者からの報告 4 件 + CI の待ち時間。すべて 測ってから 直した。


1. Cockpit のタイルが真っ黒 (4 体以上で発症)

ミニ端末は PTY を縮めない代わりに terminal::grid_rect が受け皿になっているが、
グリッドの最終行を枠の下端に合わせていた。代替画面をやめて出力を積む CLI は
起動直後グリッドの上のほうしか使わないので、最終行を下端に合わせると
空行だけが映る。

実測 (実 PTY・40 行のグリッド):

CLI 中身のある最下行 カーソル行 下端の空き
claude 2.1.234 17 14 22 行
codex 8 8 31 行
gemini 37 39 0 行

タイルに 15 行しか入らないと映るのは 25〜39 行目 = 全部空行。gemini だけ
無事だったのは画面を埋めるから。

直し方: live_row_count を新設し、最終行ではなく 生きている最下行 を
下端へ合わせる。カーソルは中身の 1 つ下までしか追わない (描き終えて隅へ寄せる
CLI を追うと、結局は空行の下半分を映してしまうため)。


2. Codex を選んだのに Claude へ文章が行く

egui 0.29 の Button / SelectableLabel は allocate_* の 自動採番 から
ID を作る。タブ列は [● 印][タブ][◆ 未読][⏳ 制限] の並びで、未読印は出力の
たびに付いたり消えたりする
。egui は押した (press) フレームの ID を離した
(release) フレームで照合するので、その 2 フレームの間に印が増減すると
押したのとは別のタブが選ばれる。

ヘッドレスで再現した (e2e::widget_id_shift_tests) — tab2 を押したのに
tab3 が選ばれる
。選び直しは入力先そのものなので、打った文章は隣の
エージェントへ行く。同じ形が Cockpit のタイル見出し (✕ を押して ⟳ が発火) と
💬 セッション一覧にもあった。さらに Cockpit のタイル枠の ID が 添字 由来
だったため、1 体閉じると添字がずれて別セッションの枠になる経路もあった。

直し方: タブ・タイル・サイドバー行の ID を セッション ID から 作る。
行の中の条件付きボタン (🛡 / ⊞) でも同じずれが起きるので、ボタンも 1 個ずつ
ID を固定する。


3. 途中で全画面表示されなくなる

terminal::draw に一方向のラッチ (size_owned) があり、一度でも下部パネルが
低い高さで描くと、その大きさが永久に固定される
。その後 Cockpit を全画面で
開いても端末は小さいままなので、grid_rect が最終行を下端へ合わせて
枠の上 3/4 が空白になる。

ラッチが守っていた「縮めると CLI が会話を描き直して履歴が二重になる」は
実 PTY で計ると再現しなかった (claude 2.1.234 /
CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1、起動が落ち着いた後の再送回数):

操作 バナー再送 追加バイト
動かさない (対照) 0 回 44 B
50 → 15 行 (縮める) 0 回 3,087 B
15 → 50 行 (広げる) 0 回 4,071 B

Ink の静的出力は一度きりなので、描き直されるのは入力欄まわりだけだった。
ラッチを外し、ライブ枠は いつでも広げてよい ことにした。


4. 看板を出していないときの幅が他のビューへ残る

看板のライブペインは kanban::main_rects の作りから 「全画面 = 窓幅いっぱい」と
「分割 = 細い」の 2 通りの幅
を持つ。従来はライブ枠が桁数も cols.max(cur_c) で
持ち越していたので、片方の幅が Cockpit や下部パネルへそのまま残り、文字が右へ
見切れる。

横は必ずいま描いている枠に合わせる。 見切れは UI の原則 (「どの幅でも
見切れない」) に反するので持ち越さない。縦は従来どおりライブ枠からは広げるだけ。

規則は純関数 terminal::next_pty_size(cur, frame, owner) へ切り出して表で固定した
(2 か所で計算すると、片方だけ直しても静かにずれる)。


5. CI が 13 分終わらない → 5 分予算へ

止まっていたのは cargo ではなく msrv ジョブの apt が 13 分無反応。
同じ瞬間の fast (ubuntu) は 同じ apt が 58 秒だった — ミラー 1 本が黙って
詰まっただけ。素の apt は HTTP に既定の時限を持たないので、詰まったミラーを
掴むと ログに 1 バイトも出さずに ジョブの上限まで待つ。

実測 6 run の wall-clock:

ジョブ 平均 最大
fast (windows) 6m48 8m16 ← 6 回とも 5 分超
msrv 3m34 15m00 (apt 詰まり)
mobile screen 4m51 15m00
fast (macos) 4m07 5m13
fast (ubuntu) 3m43 4m04

直したこと

  1. fast を分割 — windows 3 分割 / macos 2 分割 (--partition count:M/N)。
    Windows はテストの段だけで 390 秒、固定費 100 秒。nextest はテスト 1 件 =
    1 プロセスなので、プロセス生成が高価な Windows では 4,950 件ぶんの起動費が
    丸ごと効き、1 台では削れない。
  2. apt を道具 1 本に寄せて時限を入れる (tools/ci-linux-deps.sh)。3 段:
    apt 自身の Acquire::*::Timeout=15 + Retries=3 / コマンドごとの外側の時限 /
    試行回数の上限。timeout(1) が無い環境でも 素の sh だけ で同じことをする。
    ワークフロー 3 本・計 10 か所の複製を全部これへ差し替えた。
  3. --self-test でわざと固まらせ、時限が効くことを毎回証明する。
    これで「時限切れ 124 を握り潰して rc=0 と出る」バグが実際に見つかった。
  4. timeout-minutes を実測の 2〜3 倍へ (15 分 → 6〜10 分)。15 分のままだと
    「5 分予算」と矛盾し、固まったときに 15 分待たされるだけ。

結果 (この PR の run):

ジョブ 前 後
fast (windows) 8m16 4m36 / 3m50 / 4m4
msrv 15m00 (ハング) 1m19
fast (macos) 5m23 2 分割済み (次の run で確認)

番人 (追加 12 本)

全部わざと壊して赤になることを確認した。 空回りする検査は残していない。

テスト 守るもの
terminal::tests::空の下半分ではなく生きている最下行を下端に合わせる タイルが真っ黒にならない
terminal::tests::生きている最下行はカーソルと中身の遠いほう カーソルの追い方
terminal::pty_size_tests::主は枠のとおりに持つ 大きさの規則
terminal::pty_size_tests::ライブ枠は縦を縮めない 全画面で空白にならない
terminal::pty_size_tests::横は他のビューの幅を持ち越さない 幅が見切れない
e2e::widget_id_shift_tests::自動採番のままだと押したのと違うタブが選ばれる 壊れている側の再現
e2e::widget_id_shift_tests::行の中身が増減しても押したタブが選ばれる 直ったことの確認
app::ui_wiring_tests::エージェントタブのidはセッションidから作る 実際の配線
app::ui_wiring_tests::行ごとのボタンのidはセッションidから作る 実際の配線
cli::tests::ciのaptは時限つきの道具越しに呼ぶ 無音の 13 分を作らない
cli::tests::ciのジョブ上限は予算の範囲に収まっている 15 分の上限を戻さない
cli::tests::windowsのテストは分割して五分予算に収める 分割を外さない

再現テストだけだと実装が元へ戻っても緑のままなので、実際の配線を読む番人と
対にしてある。

検証

  • tools/verify.sh --all … 4954 件緑
  • tools/verify.sh --lint … clippy 緑・警告ゼロ
  • tools/windows-check.sh … 緑
  • actionlint … 全ワークフロー緑
  • Linux … 手元の Docker が未起動のため未確認 (CI の ubuntu が見ている)

🤖 Generated with Claude Code

tacyan and others added 4 commits August 18, 2026 08:00
利用者からの報告 2 件を直す。どちらも「複数エージェントを起動したときだけ」出る。

## 1. 4 体以上でタイルが真っ黒 (起動しているか分からない)

ミニ端末は PTY を縮めない代わりに `grid_rect` が受け皿になっているが、
**グリッドの最終行**を枠の下端に合わせていた。代替画面をやめて出力を積む
CLI は起動直後グリッドの上のほうしか使わないので、下端を合わせると
**空行だけが映る**。

実測 (実 PTY・40 行のグリッド・下端の空き):
  claude 2.1.234 … 22 行 (中身 0〜17 行 / カーソル 14 行)
  codex          … 31 行 (中身 0〜8 行  / カーソル 8 行)
  gemini         …  0 行 (画面を埋めるので無事)

タイルに 15 行しか入らないと映るのは 25〜39 行目 = 全部空行。
4 体以上でタイルが縮むと必ず出る。

`live_row_count` を新設し、最終行ではなく**生きている最下行**を下端へ
合わせる。カーソルは中身の 1 つ下までしか追わない (描き終えて隅へ寄せる
CLI を追うと、結局は空行の下半分を映してしまう)。画面を埋める CLI では
1 ピクセルも変わらない。

## 2. Codex を選んだのに Claude へ文章が行く

egui 0.29 の `Button` / `SelectableLabel` は `allocate_*` の**自動採番**から
ID を作る。エージェントのタブ列は `[● 印][タブ][◆ 未読][⏳ 制限]` の並びで、
**未読印は出力のたびに付いたり消えたりする**。egui は押した (press)
フレームの ID を離した (release) フレームで照合するので、その 2 フレームの
間に印が増減すると**押したのとは別のタブが選ばれる**。

ヘッドレスで再現した (`e2e::widget_id_shift_tests`):
**tab2 を押したのに tab3 が選ばれる**。選び直しは入力先そのものなので、
打った文章は隣のエージェントへ行く。

同じ形が Cockpit のタイル見出し (✕ を押して ⟳ が発火) と 💬 セッション
一覧にもある。さらに Cockpit のタイル枠の ID が**添字**由来だったため、
1 体閉じると添字がずれて別セッションの枠になる経路もあった。

タブ・タイル・サイドバー行の ID を**セッション ID から**作る。ボタンも
1 個ずつ ID を固定する (行の中の条件付きボタン 🛡 / ⊞ で同じずれが起きる)。

## 番人

追加 6 本。**全部わざと壊して赤になることを確認した**:
  terminal::tests::空の下半分ではなく生きている最下行を下端に合わせる
  terminal::tests::生きている最下行はカーソルと中身の遠いほう
  e2e::widget_id_shift_tests::自動採番のままだと押したのと違うタブが選ばれる
  e2e::widget_id_shift_tests::行の中身が増減しても押したタブが選ばれる
  app::ui_wiring_tests::エージェントタブのidはセッションidから作る
  app::ui_wiring_tests::行ごとのボタンのidはセッションidから作る

再現テストだけだと実装が元へ戻っても緑のままなので、**実際の配線を読む
番人**と対にしてある。

検証: tools/verify.sh --all (4948 件緑) / --lint (clippy 緑・警告ゼロ) /
tools/windows-check.sh 緑。Linux は Docker 未起動のため未確認。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
利用者から「テストが 13 分終わらない」。**測ってから直した。**

## 何が起きていたか (run 32078628866)

止まっていたのは cargo ではなく **`msrv` ジョブの apt が 13 分無反応**。
同じ瞬間に走っていた `fast (ubuntu)` の**同じ apt は 58 秒**で終わっている
— ミラー 1 本が黙って詰まっただけだった。素の apt は HTTP に既定の時限を
持たないので、詰まったミラーを掴むと**ログに 1 バイトも出さずに**ジョブの
上限 (15 分) まで待つ。

同じ 2 行がワークフロー 3 本に**計 10 か所**複製されていたので、片方だけ
直しても次はもう片方で固まる。

## 実測 (6 run ぶんの wall-clock)

  fast (windows)  321 / 387 / 387 / 389 / 470 / 496 秒 … **6 回とも 5 分超**
  fast (macos)    194 / 204 / 235 / 266 / 270 / 313 秒
  fast (ubuntu)   209 / 218 / 219 / 221 / 227 / 244 秒
  msrv            (apt が詰まると 900 秒 = 上限)
  mobile screen   (一度 900 秒 = 上限)

Windows の内訳は**テストの段だけで 390 秒**、固定費 100 秒。nextest は
テスト 1 件 = 1 プロセスなので、プロセス生成が高価な Windows では 4,950 件
ぶんの起動費が丸ごと効く。**1 台では削れない。**

## 直したこと

1. **Windows の fast を 3 分割** (`--partition count:M/3`)。固定費は各シャードに
   残るが並列なので wall-clock は max であって sum ではない: 100 + 390/3 ≒ 230 秒。
   ubuntu / macos は 1 台のままで予算内なので分けない (macOS は課金 10 倍)。
   `vendored vt100 tests` は先頭シャードだけが回す。

2. **apt を道具 1 本に寄せて時限を入れる** (`tools/ci-linux-deps.sh`)。3 段:
   apt 自身の `Acquire::*::Timeout=15` + `Retries=3` / コマンドごとの外側の時限 /
   試行回数の上限。`timeout(1)` が無い環境でも**素の sh だけ**で同じことをする
   (外部道具に頼ると、道具が無い環境で時限が黙って消える)。
   ワークフロー 3 本・計 10 か所の複製を全部これへ差し替えた。

3. **`--self-test` でわざと固まらせ、時限が効くことを証明する。**
   空回りする時限を残さない (実際に「時限切れ 124 を握り潰して rc=0 と出る」
   バグがこれで見つかった — `if …; then return 0; fi` の後で `$?` を読んでいた)。

4. **timeout-minutes を実測の 2〜3 倍へ**: 15 分 → 6〜10 分。
   15 分のままだと「5 分予算」と矛盾し、固まったときに 15 分待たされるだけ。
   mobile screen は 3 段の順序 (`ZV_DEADLINE_MS < ZV_WALL_S < timeout-minutes`)
   を保ったまま 10/11/15 分 → 6/7/8 分。

## 番人 (追加 3 本・全部わざと壊して赤を確認)

  cli::tests::ciのaptは時限つきの道具越しに呼ぶ
  cli::tests::ciのジョブ上限は予算の範囲に収まっている
  cli::tests::windowsのテストは分割して五分予算に収める

検証: actionlint 全ワークフロー緑 / tools/verify.sh --all 4951 件緑 /
--lint (clippy) 緑・警告ゼロ。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
利用者からの報告 2 件。どちらも「端末の大きさを決める規則」1 か所が原因。

## 1. 途中で全画面表示されなくなる

`terminal::draw` に一方向のラッチ (`size_owned`) があった。ねらいは往復の
リサイズを避けることだが、**一度でも下部パネルが低い高さで描くと、その
大きさが永久に固定される**。その後 Cockpit を全画面で開いても端末は
小さいままなので、`grid_rect` が最終行を下端へ合わせて**枠の上 3/4 が
空白**になる (変更タブの後に起きやすいのは、あちらが端末パネルを畳んで
高さを変えるため)。

ラッチが守っていた「縮めると CLI が会話を描き直して履歴が二重になる」は
**実 PTY で計ると再現しなかった** (claude 2.1.234 /
`CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`、起動が落ち着いた後の再送回数):

  動かさない … 0 回 (44B) / 50→15 行 … 0 回 (3.0KB) / 15→50 行 … 0 回 (4.1KB)

Ink の静的出力は一度きりなので、描き直されるのは入力欄まわりだけだった。
広げる側と縮める側の費用もほぼ同じ。ラッチを外し、ライブ枠は**いつでも
広げてよい**ことにした。

## 2. 看板を出していないときの幅が他のビューへ残る

看板のライブペインは `main_rects` の作りから**「全画面 = 窓幅いっぱい」と
「分割 = 細い」の 2 通りの幅**を持つ。従来はライブ枠が桁数も
`cols.max(cur_c)` で持ち越していたので、片方の幅が Cockpit や下部パネルへ
そのまま残り、文字が右へ見切れる。

**横は必ずいま描いている枠に合わせる。** 見切れは UI の原則
(「どの幅でも見切れない」) に反するので持ち越さない。縦は従来どおり
ライブ枠からは広げるだけ — 足りないぶんは `grid_rect` が
「生きている最下行」を映して吸収する。

## 規則は純関数 1 本に

`terminal::next_pty_size(cur, frame, owner)` へ切り出し、表で固定した。
2 か所で計算すると、片方だけ直しても静かにずれる。

## 番人 (追加 3 本・全部わざと壊して赤を確認)

  terminal::pty_size_tests::主は枠のとおりに持つ
  terminal::pty_size_tests::ライブ枠は縦を縮めない
  terminal::pty_size_tests::横は他のビューの幅を持ち越さない

`app::deck_wiring_tests::ライブ枠は端末の大きさを持たない` も新しい規則へ
更新し、ラッチが戻ったら赤くなるようにした (見るのは `draw` の中だけ・
コメント行は除く — 範囲を広げると経緯の文章を拾って空回りする)。

検証: tools/verify.sh --all 4954 件緑 / --lint (clippy) 緑・警告ゼロ /
tools/windows-check.sh 緑。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
前のコミットで Windows は 8m16 → 3m50〜4m36 に収まったが、
**macos が 5m23 で残っていた** (テストの段 282 秒)。この段は 173〜282 秒と
ばらつきが大きく、1 台では予算をまたぐ。

macOS ランナーは課金 10 倍だが、**固定費が 40 秒しかない**ので 2 台にしても
合計時間はほぼ変わらないまま wall-clock が半分になる。
ubuntu は 1 台で 2m49 と余裕があるので分けない。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tacyan tacyan changed the title fix: Cockpit のタイルが真っ黒・押したのと違うエージェントへ文章が行く fix: Cockpit の真っ黒タイル・別エージェントへの誤送信・端末の大きさ + CI を 5 分予算へ Aug 17, 2026
@tacyan
tacyan merged commit 53e1b10 into main Aug 18, 2026
25 checks passed
@tacyan
tacyan deleted the fix/cockpit-blank-tiles-and-widget-id-shift branch August 18, 2026 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant