codiva/https github com takecchi codiva pull 13 - #134
Merged
Conversation
takecchi
commented
Aug 21, 2026
Owner
- feat: hand conversation history across agents
- fix: deliver agent handoff through adapters
- fix: 会話引き継ぎの取りこぼしと Codex の argv 上限に対処する
エージェント切替の引き継ぎ(#133)をレビューして見つかった問題を直す。 - 引き継ぎの予算を**文字数から UTF-8 バイト**へ(`MAX_HANDOFF_TRANSCRIPT_BYTES`)。 Codex は指示文を argv で渡すので、日本語(1 文字 3 バイト)の会話が 80,000 文字 まで載ると最大 240 KiB になり、Linux の `MAX_ARG_STRLEN`(131,072 バイト)に 当たって `codex exec` の起動そのものが `E2BIG` で落ちる。しかも解除点が `thread.started` なので、以後どのターンも同じ理由で落ち続けてセッションが詰む。 macOS には per-arg 上限が無いため手元では再現しない。 - Grok: 立ち上げ中に `Ctrl+C` されたターンは `runTurn` が丸ごと捨てるのに、引き継ぎだけ 無条件に消費していた(1 回きりなので切替の文脈が黙って失われる)。`runTurn` が 「実際に投げたか」を返し、投げたときだけ落とす。 - 引き継ぎは provider にはユーザーメッセージとして届くので CLI のトランスクリプトにも そう残る。復元で `stripHandoff` を通し、詳細ビューと `lastUserInstruction` に 漏れないようにする(漏れると次の切替で引き継ぎが入れ子に写る)。 - 会話を載せられなかったときに「会話を下に写した」と名乗らないようにする (ログの無い復元セッションで嘘になる)。 - `/agent` ダイアログの注意書きが「会話の文脈は引き継がれません」のままだった (ja/en とも更新)。ユーザーが実際に読む唯一の説明。 - `composeSystemPrompt` の `handoff` 引数が呼ばれなくなっていたので削除。 - 実測を追記: `codex exec resume` も `thread.started` を出す(0.148.0)。 - アダプタ 3 本の引き継ぎ受け渡しにテストが 1 件も無かったので追加 (Claude / Codex / Grok + 中断・スレッド未開始の経路)。 - docs / rules / CLAUDE.md を実装に合わせて更新。
takecchi
marked this pull request as ready for review
August 21, 2026 10:42
4 tasks
takecchi
added a commit
that referenced
this pull request
Aug 22, 2026
## 概要 マージ済みの #133(エージェント切替時の会話引き継ぎ)を起点に、**現在の main** を 切替まわり・provider アダプタ・一覧の描画の 4 系統でレビューし、実害のあるものを直した。 #133 自体の設計(`AgentRunOptions.handoff` で最初のユーザープロンプトに前置する)は妥当で、 ここで直しているのは**その周辺の取りこぼし**と、Phase D で入った切替機能そのものの穴。 > なお #134 は #133 と同一内容の重複 PR で、マージ結果のツリー差分はゼロ(実害なし・ > 履歴に空コミットが 1 つ残っているだけ)。 ## 重い順 ### 1. 切替で畳んだ run のイベントを畳み込んでいた(resume id が混線する) `setAgent()` は入力キューを閉じて `this.run` を捨てるが、`for await` は**そのイテレータを 掴んだまま**なので古い provider のイベントは切替後にも届く。一方 `attribution` はもう切替先。 - 遅れて届いた `session_started`(Claude の `system/init` は起動に 1〜3 秒かかるので、その間に `/agent` を押せる)で **前任者の resume id が切替先の id として保存**され、以後 `codex exec resume <claude の uuid>` を投げ続ける。`agentSessions` と `state.json` にも 焼き付くので往復切替でも直らず、本物の Claude の id は失われる - 古いターンの `turn_completed` で切替済みのセッションが `completed` に戻り、**auto-PR と 完了通知まで走る**(`run.interrupt()` は best-effort なのでこの競走は普通に負ける) → 世代カウンタ(`epoch`)を持ち、世代が変わった run のイベントは畳まずに `break` (= `run.return()` で古いストリームも閉じる)。`catch` の失敗記録と「最後の砦」も同じガードの内側へ。 ### 2. 未使用の provider へ切り替えたセッションが `state.json` から消える `agent_switched` は切替先が初めての provider だと `sdkSessionId` を undefined にする。 保存条件が「現在の id があること」だったため、**`/agent` で切り替えて何も送らずに終了した セッションが state.json から丸ごと消えていた**(戻るための `agentSessions.claude` は残っている のに worktree だけが孤児になり、タイトル・コスト・PR 参照・TODO も失われる)。しかも `persistRelevantChanged` は `agent` / `agentSessions` / `sdkSessionId` の変化で即保存するので、 切替した時点で消える。 → 条件を「**どこかに** resume 用の id がある」へ。読み込み側も同条件で受理し、 `PersistedSession.sdkSessionId` を optional に。 ### 3. Grok の質問が既定モードで自動 allow されていた(ダイアログが一度も出ない) 質問の判定が **ツール名**(`AskUserQuestion`)だったため、Grok の `_x.ai/ask_user_question` は `auto`(既定)で自動 allow。`answers` の無い allow は provider には「ユーザーは答えなかった」と 同じなので、**質問がダイアログに出ないまま「承諾した」と返っていた**。 → 判定を `kind: 'question'`(アダプタが正規化した種別)に統一(`isQuestion` の 1 箇所)。 ### 4. 引き継ぎが「渡る前」に使い切られていた `Session` が `open()` の時点で引き継ぎを捨てていたので、**未ログインの CLI へ切り替えて失敗 → ログインして送り直す**という普通の経路で 1 回きりの引き継ぎが黙って消えていた(Claude は run が throw して終わるだけなので、アダプタ側で「渡るまで持つ」保険も効かない)。 → `AgentRunRequest.onHandoffDelivered()` を新設し、**渡ったと報告されるまで `Session` が持つ**。 報告点は Claude = プロンプトを `yield` した後 / Codex = `thread.started` / Grok = `session/prompt` の応答が合成エラーでないとき(プロセス死で届かなかったターンを渡した扱いにしない)。 ### 5. Codex の argv 上限(`E2BIG`)でセッションが詰む余地 `MAX_HANDOFF_TRANSCRIPT_BYTES` は**会話ぶん**の予算で、systemPrompt(`.codiva/prompt.md` は 無制限)とユーザーの指示文も同じ argv 1 本に載る。超えると `spawn` が `E2BIG` で落ち、 `thread.started` が来ない = 引き継ぎが解除されないので**以後どのターンも同じ理由で落ち続ける**。 → 送る直前に `fitHandoff(handoff, 残りバイト)` で会話の古い側から削る。1 ターンも載らなければ 引き継ぎを諦めて `notice` を 1 行残す(黙って捨てない)。 ### 6. 一覧のバッジ列が溢れて行のクリックがズレる(英語 UI) 行のセルはどれも `wrap="truncate-end"` を持つのに `ProgressBadge` だけ持っていなかった。 英語の `Awaiting permission`(19 セル)は 12 セル固定の列に入らないので**1 セッションが 2 行に 折り返し**、「1 セッション = 1 行」前提の `rowLineAtPoint` 以降のクリックが 1 行ずつズレて (別セッションが選ばれ、PR セルのクリックも他人の PR を開く)、最後の行はクリック自体が 捨てられていた。日本語はちょうど 12 セルだったので気付けなかった。 → `truncate-end` + 英語文言を 12 セル以内(`Permission` / `Login needed`)+ 列幅を `BADGE_COLUMN_WIDTH` として `core/layout.ts` へ + `i18n.spec.ts` に幅の番人を追加。 ### 7. その他 - **切替直後の `Ctrl+C` が効かない**(畳んだループの `finally` が新しいエージェントを起こす 予約を持っているため)→ 予約を取り消して積み残しを捨てる(Grok アダプタが立ち上げ中の 中断で指示を捨てるのと同じ考え方) - `stripHandoff` は**最後の**境目で切る(会話ログに同じ見出しが混ざると残骸がユーザー発言として 復元され、`lastUserInstruction` 経由で次の切替に入れ子で写る) - Grok: 拒否の選択肢が見つからないとき `options[0]`(実データでは `allow-once`)に落ちて **拒否が実行に化ける**のを `cancelled` に倒す / 未応答要求の待ち行列を**接続ごと**に持つ (死んだプロセスの後片付けが次のプロセスの `initialize` を失敗させ、健全なプロセスを殺していた)/ readLoop が異常終了したら子を畳む(同じ worktree に 2 本目が立つ)/ プロセス死の判定を JSON-RPC の code ではなく接続のフラグで(`-32000` は Grok 自身も使う)/ `GROK_HOME=""` で `auth.json` を相対パスで読んでいた - Codex: スレッド id が分かっているクラッシュ(panic = exit 101 / SIGKILL = code null)を `failed`(終端 = 再開の導線が消える)ではなく resumable な `connection` へ - 受理ガードに、パーサが無条件に読むフィールドを追加(Codex の `reasoning` / `agent_message` の `text`、`command_execution.command`、Grok の `content` / `plan` の要素)— 通すと TypeError が ターンのストリームごと殺す - 準備中(worktree 作成中)のセッションに既定エージェントを載せる(`Claude` と嘘の名前が出て、 エージェント列が一瞬現れて一覧が組み直される) - 詳細の `/agent` で同じエージェントを選び直したときにエラーを出さない / `/login` の未対応 メッセージにエージェント名を差し込む / `/agent` のヒントの全角区切りを `theme.ts` の記号へ / ログインダイアログ表示中のフッタヒント / ダイアログのアンマウントでログインプロセスを畳む ## 直していない既知の制約(ドキュメントに明記した) **Claude では切替直前に送った追加指示が古い CLI 側のキューに残る。** Claude Agent SDK は プロンプトの `AsyncIterable` を先読みするので、ターン中の追加指示はその場で CLI の stdin へ 渡り、`AsyncQueue.pending` は 0 のまま(= `drain()` で移せない)。SDK の `interrupt()` は `still_queued` として uuid を返すが、取り消すには送信時に uuid を打って `cancel_async_message` を 送る必要があり、実装していない。Codex / Grok は消費が遅延評価なので移し替えが効く。 `docs/ARCHITECTURE.md` と `.claude/rules/session-domain.md` に限界として書いた。 ## テスト 追加した番人(いずれも修正前は落ちる): - `session.spec.ts`: 畳んだ run の遅延イベントを無視する / 渡る前に死んだ run の引き継ぎを保つ / 切替直後の `Ctrl+C` で予約された再起動を取り消す / provider が何と呼んでいても質問は上げる - `persistence.spec.ts`: 未使用 provider へ切り替えたセッションを保存・復元できる - `agent-handoff.spec.ts`: `fitHandoff` の 4 ケース / 境目が本文に混ざっても最後で切る - `codex-adapter.spec.ts`: argv 上限で引き継ぎを削る / クラッシュを resumable にする - `grok-adapter.spec.ts`: 拒否できる選択肢が無ければ `cancelled` / 死んだプロセスの後片付けが 次のプロセスを巻き込まない - `codex-events.spec.ts` / `grok-events.spec.ts`: 受理ガードの追加ケース - `i18n.spec.ts`: バッジは一覧の列幅(12 セル)に収まる CI と同じ 4 点をローカルで通した: - [x] `lint`(biome、268 ファイル) - [x] `typecheck` - [x] `test` — 118 files / 3021 tests passed - [x] `build` 体感確認(実際に `/agent` で切り替える・英語 UI で行をクリックする)は TTY と各 CLI の認証が 要るため未実施。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.