Compare commits

...

2 Commits

Author SHA1 Message Date
44bc35bd31
docs(tickets): 消し忘れチケットども 2026-05-03 01:16:22 +09:00
f8a3a7838b
chore: TODOから[ ]を削除 2026-05-03 01:08:43 +09:00
8 changed files with 21 additions and 557 deletions

View File

@ -1 +1,2 @@
_staging
memory

View File

@ -1,5 +1,5 @@
[memory]
extract_threshold = 20000
extract_threshold = 50000
consolidation_threshold_files = 10
# consolidation_threshold_bytes = 0

44
TODO.md
View File

@ -1,27 +1,21 @@
- [ ] Workflow / Skills
- [ ] 内部 Worker / 内部 Pod の Workflow 化 → [tickets/internal-worker-workflow.md](tickets/internal-worker-workflow.md)
- [ ] Agent Skills を Workflow として ingest → [tickets/agent-skills.md](tickets/agent-skills.md)
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
- [ ] Resume 時の Scope claim の改善 → [tickets/resume-scope-claim.md](tickets/resume-scope-claim.md)
- [ ] Pod CLI: マニフェスト関連フラグの整理 → [tickets/pod-cli-manifest-flags.md](tickets/pod-cli-manifest-flags.md)
- [ ] OpenAI Responses: sampling パラメータの取り扱い → [tickets/responses-sampling-params.md](tickets/responses-sampling-params.md)
- [ ] OpenAI Responses: prompt_cache_key 送出 → [tickets/responses-prompt-cache-key.md](tickets/responses-prompt-cache-key.md)
- [ ] llm-worker のエラー耐性
- [ ] HTTP transient リトライ → [tickets/llm-worker-transient-retry.md](tickets/llm-worker-transient-retry.md)
- [ ] ストリーム途中失敗時の継続 → [tickets/llm-worker-stream-continuation.md](tickets/llm-worker-stream-continuation.md)
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
- [ ] TUI 拡充
- [ ] フルスクリーン化によるオーバーホール → [tickets/tui-fullscreen-overhaul.md](tickets/tui-fullscreen-overhaul.md)
- [ ] Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md)
- [ ] ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
- [ ] auto-kick 由来ターンが描画されない → [tickets/tui-pod-event-render.md](tickets/tui-pod-event-render.md)
- [ ] サブミット入力
- [ ] FileRef リゾルバ → [tickets/submit-file-ref-resolver.md](tickets/submit-file-ref-resolver.md)
- [ ] Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
- [ ] メモリ機構
- [ ] 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
- [ ] Phase 2 累積入力トークン上限の撤去 → [tickets/memory-consolidation-drop-input-cap.md](tickets/memory-consolidation-drop-input-cap.md)
- [ ] セッション内 TODO ツール(注意機構付き) → [tickets/session-todo.md](tickets/session-todo.md)
- [ ] セッションメトリクス: Extension 経由の汎用計測レーン(最初の利用者は Prune → [tickets/session-metrics.md](tickets/session-metrics.md)
- Workflow / Skills
- 内部 Worker / 内部 Pod の Workflow 化 → [tickets/internal-worker-workflow.md](tickets/internal-worker-workflow.md)
- Agent Skills を Workflow として ingest → [tickets/agent-skills.md](tickets/agent-skills.md)
- パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
- Resume 時の Scope claim の改善 → [tickets/resume-scope-claim.md](tickets/resume-scope-claim.md)
- Pod CLI: マニフェスト関連フラグの整理 → [tickets/pod-cli-manifest-flags.md](tickets/pod-cli-manifest-flags.md)
- llm-worker のエラー耐性
- HTTP transient リトライ → [tickets/llm-worker-transient-retry.md](tickets/llm-worker-transient-retry.md)
- ストリーム途中失敗時の継続 → [tickets/llm-worker-stream-continuation.md](tickets/llm-worker-stream-continuation.md)
- ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
- TUI 拡充
- Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md)
- ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
- auto-kick 由来ターンが描画されない → [tickets/tui-pod-event-render.md](tickets/tui-pod-event-render.md)
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
- メモリ機構
- 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
- セッション内 TODO ツール(注意機構付き) → [tickets/session-todo.md](tickets/session-todo.md)
- セッションメトリクス: Extension 経由の汎用計測レーン(最初の利用者は Prune → [tickets/session-metrics.md](tickets/session-metrics.md)
- ワークスペースのメモリーをLintするヘッドレスCLI
- system-reminder 注入機構の汎用化2件目の利用者が出た時に検討。タグ形式と「履歴を汚さない」原則は session-todo で先行確立)

View File

@ -1,118 +0,0 @@
# OpenAI Responses: prompt_cache_key 送出によるキャッシュ有効化
## 背景
codex-oauth 経由 (ChatGPT backend) の OpenAI Responses API において、
プロンプトキャッシュが事実上効いていない。実セッションログ
(`019de419-...`, 171 turn / 累計入力 22.2M token) で `cache_read_tokens`
が全 turn 0、最新セッション (`019de48f-...`, 5 turn) でも 0。
直近のパース修正 (`events.rs` の `ResponsesUsage`
`input_tokens_details.cached_tokens` を追加) で計測経路は復旧したが、
それでも 0 が観測される → **server 側でそもそもキャッシュが効いていない**
原因は codex-rs の実装で確定:
```rust
// codex-rs/core/src/client.rs:853
let prompt_cache_key = Some(self.client.state.conversation_id.to_string());
```
ChatGPT backend では `prompt_cache_key` をリクエストに含めないと
プロンプトキャッシュが期待通りに動かない (org/project ハッシュが
別 conversation と衝突しやすく、ヒット率が著しく落ちる)。Codex は
conversation 単位の安定キーを毎リクエスト付けて、その名前空間内で
prefix をキャッシュさせている。
insomnia 側の `ResponsesRequest` には `prompt_cache_key` フィールドが
存在せず、`Request` 構造体にも会話/セッション単位の安定キー概念が無い。
このため codex-oauth で長尺の Run を走らせると毎 turn 全 prefix を
従量課金している。
## 方針
`Request` に provider-agnostic な `cache_key: Option<String>`
足し、`OpenAIResponsesScheme` がそれを `prompt_cache_key` として
送る。pod 側は LLM 呼び出し時に `SessionId` をキーとして渡す。
他 scheme (Anthropic / Gemini / OpenAI Chat / Ollama) はフィールドを
無視する。既存の `cache_anchor` (Anthropic 用 prefix anchor) と
同じ「キャッシュヒントを Request に載せ、効く provider だけ拾う」
規約に揃える。
### Fork との関係
`session-store::fork` / `fork_at` はいずれも新 `SessionId` を発行する。
本チケットでは **新 fork = 新 cache_key** とする (素直に
`SessionId.to_string()` を渡す)。fork 直後の cache 明示ヒットは失われる
が、OpenAI Responses は automatic prefix matching も走るため完全に
冷えるわけではない。fork 越しに親の cache_key を継承して明示ヒットも
残す最適化は別チケットで検討する (本ticketの範囲外)。
## 要件
### llm-worker 側
- `Request``cache_key: Option<String>` を追加 (`types.rs:442`
`cache_anchor` の隣)。doc コメントで「会話単位の安定キー。
prompt_cache_key として送られる (OpenAI Responses)。
prefix anchor を持たない provider は無視」を明記
- ビルダ `Request::cache_key(impl Into<String>)` を追加
- `OpenAIResponsesScheme::build_request``request.cache_key.clone()`
`ResponsesRequest::prompt_cache_key` にセット
- `ResponsesRequest``prompt_cache_key: Option<String>` を追加
(`#[serde(skip_serializing_if = "Option::is_none")]`)
- 他 scheme (`anthropic`, `gemini`, `openai_chat`) は touch しない
(Request の新フィールドを未参照のまま残す)
### pod 側
- LLM クライアントに渡す `Request` を組み立てる箇所で
`cache_key(session_id.to_string())` を入れる。少なくとも以下:
- 主 Run の LLM 呼び出し (`pod.rs` の Run / Worker 経路)
- compactor worker
- memory extract worker
- `SessionId``SharedState::session_id` から取得できる
(`shared_state.rs:21`)
- compactor / extract のように pod の中で派生する worker でも
同じ `session_id` を使う。これにより pod 内のすべての LLM
呼び出しが同一 cache_key 名前空間で動き、prefix が共有される
ところでヒットが期待できる
### docs
- `docs/research/` 配下に `openai_responses_prompt_cache_key.md`
(仮) を追加し、「ChatGPT backend では prompt_cache_key 必須」
「codex-rs の挙動」「insomnia での Fork 方針」を残す。
既存の `openai_responses_max_output_tokens.md` と並びで置く
## 完了条件
- `Request::cache_key("abc")` で組んだリクエストが、
`OpenAIResponsesScheme::build_request`
`prompt_cache_key: "abc"` を含む body を生成する (unit test)
- `cache_key = None` のときは body に `prompt_cache_key` キーが
載らない (`skip_serializing_if`) (unit test)
- pod の Run で codex-oauth + Responses を使ったとき、2 turn 目
以降の `cache_read_tokens` が 0 でない (実セッションログで確認)
- `cargo check` / `cargo test``llm-worker`, `provider`, `pod`
で通る
## 範囲外
- Fork 越しのキャッシュ継承 (`forked_from` を辿って root の
cache_key を継承する最適化)。別チケット
- 公式 OpenAI Responses API (非 ChatGPT backend) での
`prompt_cache_key` 必要性検証。少なくとも害は無いので両経路で
同じ値を送って良い
- compaction で prefix が大きく書き換わる経路の cache_key 戦略
(compaction 後は prefix がほぼ別物なので、ヒット率を最大化する
なら compaction 直後だけ別 key にする手もあるが、まずは単純に
session_id 一本で動かす)
- `cache_anchor` (Anthropic 用) と `cache_key` (Responses 用) の
統合。両者は別概念 (前者は prefix の境界 index、後者は
名前空間キー) なので並立させる
## Review
- 状態: Approve
- レビュー詳細: [./responses-prompt-cache-key.review.md](./responses-prompt-cache-key.review.md)
- 日付: 2026-05-02

View File

@ -1,63 +0,0 @@
# Review: OpenAI Responses prompt_cache_key 送出によるキャッシュ有効化
## 前提・要件の確認
### llm-worker 側
- `Request``cache_key: Option<String>` を追加し doc を整備:
`crates/llm-worker/src/llm_client/types.rs:458-465`
`cache_anchor` の直下、要件通りの位置。doc にも「会話単位の安定キー」「`prompt_cache_key` として送られる」「持たない provider は無視」が明記されている。
- `Request::cache_key(impl Into<String>)` builder:
`crates/llm-worker/src/llm_client/types.rs:546-552`。要件通り。
- `OpenAIResponsesScheme::build_request``request.cache_key.clone()``ResponsesRequest::prompt_cache_key` に投影:
`crates/llm-worker/src/llm_client/scheme/openai_responses/request.rs:228`
- `ResponsesRequest::prompt_cache_key: Option<String>``#[serde(skip_serializing_if = "Option::is_none")]` 付きで追加:
`request.rs:51-57`
- 他 scheme (`anthropic`, `gemini`, `openai_chat`) は touch されておらず、`Request::cache_key` を未参照のまま無視しているgrep 確認済)。要件通り。
### pod 側
- 主 Worker の構築 3 経路すべてで `set_cache_key(Some(session_id.to_string()))` を実施:
- `from_manifest`: `crates/pod/src/pod.rs:1915`
- `from_manifest_spawned`: 同 `:1976`
- `restore_from_manifest`: 同 `:2064`
- compactor (`summary_worker`) と memory extract (`extract_worker`) も session_id でキー付け済み:
`pod.rs:1198`, `pod.rs:1553`。要件で明示された 3 経路(主 Run / compactor / extractすべてカバーされている。
- `Worker::set_cache_key` の追加と、`build_request` 時の `request.cache_key = self.cache_key.clone()` 投影、`lock()`/`unlock()` 越しの引き継ぎ:
`crates/llm-worker/src/worker.rs:193, 399-405, 600, 1338, 1418`。状態遷移で落ちないことが確認できる。
### docs
- `docs/research/openai_responses_prompt_cache_key.md` を新規作成。
「ChatGPT backend では prompt_cache_key 必須」「codex-rs の挙動」「insomnia での Fork / Compaction 方針」「公式 API での挙動」「URL」がカバーされている。`openai_responses_max_output_tokens.md` と同じ並び。要件通り。
### 完了条件
- ユニットテスト 2 件:
`prompt_cache_key_passed_through_when_set` / `prompt_cache_key_omitted_when_none`
(`crates/llm-worker/src/llm_client/scheme/openai_responses/request.rs:540-560`)。
body にキーが乗る/省略される、両方を JSON 値で確認している。完了条件を満たす。
- `cargo check --workspace` 通過確認済(手元再実行で確認)。
- `cargo test -p llm-worker --lib` 121 件パス、`-p provider --lib` 41 件、`-p pod --lib` 133 件パスを確認。
- 「pod の Run で codex-oauth + Responses を使ったとき、2 turn 目以降の `cache_read_tokens` が 0 でない」については実セッションログ要観測(実装上は events.rs のパース修正で `cache_read_input_tokens` を埋める経路が出来ており、`prompt_cache_key` も乗ることがテストで確認できているので、計測経路としては揃っている)。これはコード単体では検証できない要件で、実走行に委ねるのが妥当。
## アーキテクチャ・スコープ
- llm-worker は provider-agnostic な `cache_key` を持ち、scheme ごとの解釈は scheme 配下で完結。`cache_anchor` (Anthropic 用 prefix index) と並立して別概念として扱う方針が doc とコードの両方で明確。低レベル基盤を歪めず、`cache_anchor` の規約パターンに素直に乗っている。
- 他 scheme (`anthropic`, `gemini`, `openai_chat`) はフィールドを未参照のまま残しており、不要な実装拡散がない。
- 範囲外項目への踏み込み確認:
- **memory consolidate worker への適用** (`pod.rs:1750`): ticket の要件節で明示されているのは Run/compactor/extract の 3 件だが、同節末尾に「compactor / extract のように pod の中で派生する worker でも 同じ session_id を使う。これにより pod 内のすべての LLM 呼び出しが同一 cache_key 名前空間で動き、prefix が共有されるところでヒットが期待できる」というポリシーが明記されている。consolidate も pod 内派生 worker なので、明示列挙されていなくとも policy の対象に当然含まれる解釈で妥当。漏れて consolidate だけ別 namespace になる方が不自然。
- **compact 後の re-key** (`pod.rs:1363`): compact 中に `self.session_id = new_session_id` (1333) で session_id 自体が入れ替わる。worker 側のキャッシュキーを古い session_id のまま放置すると、post-compact turn と extract/consolidate (これらは `self.session_id` = 新) で namespace が分裂する。範囲外の「compaction の cache_key 戦略」は「明示キーを別系統に切り替える等の最適化」を指しており、ここは「session_id 一本で動かす」という ticket 末尾の方針 (110行) を素直に維持しているだけ。むしろ re-key しない方がポリシー違反になる。
- Worker の構築・状態遷移箇所すべてで `cache_key` をハンドリング (`Worker::new` 初期化、`lock`/`unlock` 引き継ぎ) しており、後段で見落としによる空キー問題が起きない。
- `events.rs``input_tokens_details.cached_tokens` 取り込みは ticket 本文では「直近のパース修正で復旧した」前提として記述されているが、実際には未コミットだった分が今回まとめて入っている。これは本 ticket 完了の前提として必要な計測経路であり (= cache 効果が実環境で確認可能になる)、ticket の精神を満たすために必要。範囲外項目ではない。
## 指摘事項
### Blocking
なし。
### Non-blocking / Follow-up
- 完了条件の最後codex-oauth で実走行して `cache_read_tokens` が 0 でないことを実セッションログで確認は実装変更だけでは取り込めない。ticket クローズ前に 1 セッション流して `cache_read_input_tokens` がログに出ることを確認してほしい。
- ticket 本文では `cache_anchor` の隣としていたが、`Request::cache_anchor` フィールドの doc コメントが英語、`cache_key` は日本語になっている。プロジェクト方針として混在は許容されているように見えるが、両者揃える価値はある。優先度は低い。
### Nits
- `docs/research/openai_responses_prompt_cache_key.md:79-83`「Compaction との関係」セクションは ticket 範囲外項目を補足する形になっているが、現時点の実装方針 (post-compact で new session_id に re-key) と完全整合しているので有用。残してよい。
## 判断
**Approve** — ticket の前提・要件・完了条件はコード上満たされており、ticket 範囲外と明記された fork 越し継承や compaction 戦略への踏み込みも無い。consolidate worker への展開と post-compact re-key は ticket の policy 文 (「pod 内のすべての LLM 呼び出しが同一 cache_key 名前空間で動き」「session_id 一本で動かす」) に沿った最小拡張で、コードベースを歪めていない。残るは実セッションでの `cache_read_tokens > 0` 観測のみで、これは実走確認に委ねるのが妥当。

View File

@ -1,102 +0,0 @@
# OpenAI Responses: sampling パラメータの取り扱い
## 背景
ChatGPT backend (`https://chatgpt.com/backend-api/codex/responses`) は公式
OpenAI Responses API のサブセットしか受け付けず、サポート外パラメータを
含むリクエストを 400 (`Unsupported parameter: ...`) で拒否する。
受理パラメータは概ね以下に限られる(`docs/research/openai_responses_max_output_tokens.md`:
```
model, input, instructions, stream, store, include,
tools, tool_choice, reasoning, previous_response_id, truncation
```
`max_output_tokens` については先行修正 (commit `af57d5b`) で
`OpenAIResponsesScheme::send_max_output_tokens` を導入し、
`AuthRef::CodexOAuth` 経路では送らないようにしてある。
今回、同じ経路で `temperature` も 400 を返すことが確認された:
```
[notice] pod: memory Phase 1 extract failed:
Client error: API error (status: 400):
{"detail":"Unsupported parameter: temperature"}
```
加えて、Pod の compactor / extract worker は `pod.rs`
`.temperature(0.0)` をハードコードしている。「決定論的に振る舞う」程度の
動機で 0.0 が選ばれているが:
- 公式 reasoning モデル (`gpt-5`, o 系) は temperature を無視/固定する
- 他プロバイダ (Claude / Gemini / Ollama) でも 0.0 が extract / 要約に
最適という自前検証は無い
- そもそもプロバイダ既定値がそれぞれの妥当な値になっているはず
ハードコードを残す積極的理由が弱く、かつ codex-oauth で実害が出ている。
## 方針
二段で対処する。
1. **wire-level**: `OpenAIResponsesScheme`
`send_sampling_params: bool` を追加し、`AuthRef::CodexOAuth` 経路では
`false` に設定する。`false` のとき `temperature` / `top_p`
body に載せない。`max_tokens` と同じ枠組みなので構造は揃える。
2. **pod-level**: `pod.rs``.temperature(0.0)` ハードコード 2 箇所を
撤去する。プロバイダ既定値に任せる。
(2) だけでも codex-oauth の現症状は消えるが、ユーザが manifest で
明示的に `temperature` を設定しているケース(非 0.0)でも codex-oauth
配下では 400 になるため、(1) も併せて入れる。
## 要件
### Scheme 側
- `OpenAIResponsesScheme``send_sampling_params: bool` フィールドを
追加(デフォルト `true` = 公式 OpenAI API 向け)
- `with_send_sampling_params(bool)` ビルダを生やす
- `request.rs``ResponsesRequest``temperature` / `top_p`
`send_sampling_params == false` のときは `None` のまま送る
`#[serde(skip_serializing_if = "Option::is_none")]` で除外)
- `validate_config``send_sampling_params == false` かつ
`config.temperature.is_some()` または `config.top_p.is_some()`
ときに `ConfigWarning::unsupported` を返す(`max_tokens` と同じ流儀)
- `provider/src/lib.rs``SchemeKind::OpenaiResponses` 分岐で、
`AuthRef::CodexOAuth` のとき `send_sampling_params=false` を渡す
### Pod 側
- `crates/pod/src/pod.rs:1011` の compactor worker `.temperature(0.0)` を撤去
- `crates/pod/src/pod.rs:1368` の extract worker `.temperature(0.0)` を撤去
- 既存テストが落ちないことを確認(`pod.rs:2034` のテスト assert は
`RequestConfig` に直接 `temperature: Some(0.2)` を入れているので
ハードコード撤去とは独立)
### docs
- `docs/research/openai_responses_max_output_tokens.md`
「ChatGPT backend が拒否するパラメータ一覧」を補足するか、
もしくは sampling 用の研究 doc を新設して `temperature` / `top_p`
の扱いを明文化するmax_output_tokens の doc に追記する形で十分)
## 完了条件
- `OpenAIResponsesScheme::new().with_send_sampling_params(false)`
作った scheme から生成した body に `temperature` / `top_p` キーが
載らないunit test
- `provider::build_client``AuthRef::CodexOAuth` + `OpenaiResponses`
の組合せから作った client が `temperature` を含まないリクエストを送る
- pod の compaction / memory extract が codex-oauth 経由で 400 にならず
最後まで走る
- `pod.rs` から `.temperature(0.0)` のハードコードが消えている
- `cargo check` / `cargo test``llm-worker`, `provider`, `pod` で通る
## 範囲外
- `user` / `metadata` 等、現状コードで送出していない他の拒否パラメータ
- 公式 OpenAI Responses API 側の `temperature` 挙動の変更
- 「extract / 要約タスクに最適な temperature は何か」という検証
(必要になったら manifest で per-model 設定に逃がすのが筋であり、
pod.rs 内に再ハードコードはしない)

View File

@ -1,81 +0,0 @@
# サブミット入力: FileRef リゾルバ
## 背景
`tickets/submit-tui-completion.md``@<path>` が typed atom として入力され、submit 時に `Segment::FileRef { path }` で Pod へ届く経路が完成した。一方 Pod 側 (`Pod::flatten_segments` in `crates/pod/src/pod.rs`) は今 `FileRef` を見ても resolver を持たず、`Segment::flatten_to_text` の placeholder (`[unresolved file ref: ...]`) を user message に inline するだけで、Warn alert を吐いて終わっている。
ClaudeCode の `@<path>` と同等の挙動 — submit 時にファイル本文を読み、LLM context にそのまま見せる — を入れる。`compact/worker.rs` の `mark_read_required` 経路で完成済の auto-read`PodFsView::render_auto_read`と兄弟関係になる、submit 時版のリゾルバ。
## 要件
### Item 配置
履歴に永続化する形は以下の **2 つの Item** にする:
```
[..., user_message, system_message(file1), system_message(file2), ...]
```
user message 自体は今と同じく `Segment::flatten_to_text` 由来のテキスト(`@<path>` トークンが残った placeholder 込み)。直後に `[File: <path>]\n<本文>` 形式の system message を、`FileRef` の出現順に追加する。次ターン以降も LLM が見える状態で残すcompact が走った時点で既存の auto-read 機構が引き継ぐ)。
inline 結合user 1 メッセージに本文を流し込む)は採らない。
### 本文の取り扱い
- `PodFsView` (`crates/pod/src/fs_view.rs`) 経由で読む。スコープ判定は `ScopedFs` 任せ。
- 上限は通常の Tool Output と同じ `manifest::defaults::TOOL_OUTPUT_MAX_BYTES` (16 KB)。超過分は捨て、末尾に `[...truncated, <total> bytes total — use read_file for the rest]` を付ける。LLM が必要なら自分で `read_file` を呼ぶ前提。
- 非 UTF-8バイナリはリゾルバが拒否する。後述の失敗扱いに倒す。
### 失敗時の扱い
スコープ外 / NotFound / バイナリ拒否は **Alert + placeholder 残置**:
- ユーザー向け Alert を `AlertLevel::Warn` で発火(理由を含めた一文)
- 該当 segment の system message は出さないuser message 中の `[unresolved file ref: <path>]` プレースホルダーがそのまま LLM に届く)
これは「ユーザーの誤入力を早期に可視化する」狙い。silent fallback にしない。
### Worker 側 API 拡張
submit 時に user message と system messages を一つの turn の前置として履歴に積む経路を、既存の `Interceptor` action-return パターンに合わせて足す。`TurnEndAction::ContinueWithMessages(Vec<Item>)` (`crates/llm-worker/src/worker.rs:903`) と同形:
- `Interceptor::on_prompt_submit` の戻り値を拡張し、`Continue` / `Cancel(String)` に加えて `ContinueWith(Vec<Item>)` を返せるようにする
- Worker の `Locked::run``ContinueWith` を受けたら user_item の push 直後に extras を `history.extend` する
- Hook (`crates/pod/src/hook.rs`) 側の戻り値(`PromptAction`はこの拡張に乗せない。Hook は read-only な公開拡張面という設計hook.rs:8-15 のコメントを維持するため、Hook と Interceptor で戻り値型を分離する
### Pod 側の resolver 配線
- `PodFsView::resolve_file_ref(&self, path: &str, max_bytes: usize) -> Result<Item, ResolveError>` を新設。`ScopedFs` で読み、UTF-8 検証 + 16 KB 切詰めを行い `Item::system_message` を返す。エラーは `OutOfScope` / `NotFound` / `Binary` / `Io(io::Error)` を区別する
- `PodSharedState` に submit 中だけ使う stash (`Mutex<Vec<Item>>`) を一個追加。`pending_notifies` / `compact_state` と同じ流儀
- `Pod::run` で submit 直前に `Vec<Segment>` を走査して FileRef を resolver に通し、成功分は stash、失敗分は Alert に流す
- `PodInterceptor::on_prompt_submit` で stash を取り出して空でなければ `ContinueWith(items)` を返す
## 範囲外
- Knowledge / Workflow resolverそれぞれ `tickets/memory-phase2-consolidation.md``tickets/workflow.md` 側)
- 画像など binary attachment の typed メッセージ化(将来 `ContentPart::Image` 等を入れる別チケット)
- `@<path>:<line>-<line>` のような行範囲指定構文
- compact 後の auto-read との重複排除compact が user message 由来の FileRef を読み直す可能性は許容)
## 完了条件
- `@<path>` を含む submit が、user message + 解決済み system message の 2 Item として履歴に残る
- 16 KB を超えるファイルは truncate され、その旨が LLM に見える形で示される
- スコープ外 / NotFound / バイナリは Alert として通知され、LLM 側は placeholder を見るのみ
- Hook の戻り値型は据え置き、Interceptor のみ `ContinueWith` を受け付ける
- 既存ビルド・テストを壊さない
## 依存
- `tickets/submit-tui-completion.md`FileRef segment の wire 接続)
## 参照
- `crates/pod/src/pod.rs``flatten_segments`, `Pod::run`
- `crates/pod/src/fs_view.rs``PodFsView` — auto-read の隣に置く)
- `crates/pod/src/ipc/interceptor.rs``PodInterceptor::on_prompt_submit`
- `crates/pod/src/shared_state.rs`stash 追加先)
- `crates/llm-worker/src/interceptor.rs``PromptAction` 拡張)
- `crates/llm-worker/src/worker.rs:903``TurnEndAction::ContinueWithMessages` 既存パターン)
- `crates/pod/src/hook.rs:8-15`Hook と Interceptor の責務分離 doc
- `crates/manifest/src/defaults.rs``TOOL_OUTPUT_MAX_BYTES`

View File

@ -1,167 +0,0 @@
# TUI: フルスクリーン化によるオーバーホール
## 背景
現在の TUI は **ratatui の inline viewport + `insert_before`** で動いている:
- `draw()` が描くのは画面下部の 3 行固定 (separator / status / input)
- 履歴は `terminal.insert_before()` でその上に押し出し、ターミナル側のスクロールバックに残す
- 一度 `insert_before` した行はアプリから書き換え不可
このモデルが以下の構造的課題を生んでいる:
1. **Input UX が貧弱**: Input は 1 行固定、複数行入力不可、ペーストすると長文が 1 行として流れる
2. **リサイズでセパレーターが増殖**: ターミナル横幅が変わるたびに separator 行が再生成されてスクロールバックに流れ、履歴が汚れる
3. **読み返し辛い**: TUI アプリ自身にスクロール機能がなく、ターミナル側スクロールバックに完全依存。折りたたみもフィルタリングもできない
4. **ツール呼び出しが断片化**: `ToolCallStart` / `ToolCallDone` / `ToolResult` がそれぞれ別行として積まれ、1 呼び出しを連続した 1 ブロックとして見られない。`ToolCallArgsDelta` は **破棄されている** (`crates/tui/src/app.rs`)
個別対症療法では直しきれないため、**レンダリングモデルを alternate screen buffer + 全描画保持に切り替えるオーバーホール**として扱う。旧 `tui-tool-call-ui.md` は inline viewport 維持を前提にした設計だったため、本チケットに要件を吸収して削除する。
## 方針
- ratatui を alternate screen buffer で初期化し、inline viewport を捨てる
- 全履歴を TUI アプリ内の state として保持、毎フレーム再描画
- ターミナル側のスクロールバックには何も流さない(リサイズ時の汚染と永続履歴依存を同時に解消)
- 履歴の見せ方をツール呼び出しやターン単位で集約可能にし、スクロール + 折りたたみ 3 段階で密度を変えられるようにする
- ツール呼び出しは 1 呼び出し = 1 ブロック。ツール名で dispatch する **ツール毎レンダラ** のフレームワークを持つ
- Input は複数行 / CJK / ペースト プレースホルダに対応
## 要件
### レンダリングモデル
- ratatui を alternate screen buffer で起動する。`insert_before` は使わない
- アプリ終了時はスクロールバックに戻るTUI が描いたものは残らない)
- 再接続時は既存の `Event::History` で state を再構築する
- ウィンドウリサイズは state を保ったまま再レイアウトする(セパレーター増殖等は発生しない)
### レイアウト
- 画面全体を以下で構成する:
- 上部: 履歴ビュー(可変高さ、スクロール可)
- 下部: ステータス行 (1 行) + Input エリア (可変高さ)
- Input エリアが画面下部を占有しすぎないように上限を設ける(設計で決めること)
### 履歴モデル
- 最小単位は **ブロック**。種類: GreetingCard / TurnHeader / UserMessage / AssistantText / ToolCall / Notification / CompactEvent / TurnStats
- ブロックは **ターン** にグループ化される。TurnHeader / UserMessage 〜 その turn の TurnStats までが 1 ターン
- 履歴全体が state に保持され、モードに応じて各ブロックの見た目が変わる
### スクロール
- 行単位 / ページ単位のスクロール
- ターン単位のジャンプ(前のターン / 次のターン / 先頭 / 末尾)
- 末尾追従(常に最新を表示)は新規イベント到着でトリガ。ユーザーが手動で上にスクロールしている間は追従を停止
### 折りたたみモード
3 段階、全体トグルで切替。
- **detail**: 全ブロックを完全表示。ツールブロックは引数ストリーミング + 結果全体
- **normal**: 実行中のツールブロックは detail と同じ。完了後は各ブロックが概ね 5〜6 行に収まるよう圧縮
- **overview**: 各ブロックが 1 行。ツールブロックは `ToolResult.summary` をそのまま使う
### Input エリア
- 複数行、自動折り返し
- CJK 含む Unicode 表示幅に基づく正しいカーソル位置 (`unicode-width`)
- **ペースト プレースホルダ**: クリップボードから貼り付けられた文字列はプレースホルダ `[Clipboard #N | X chars, Y lines]` として入力バッファに挿入される。実テキストは裏で保持
- プレースホルダは不可分Backspace 1 回で全体が削除される。途中カーソルでの文字単位削除は不可)
- 送信時にプレースホルダが実テキストに展開されて Pod に送られるPod 側には `#N` は見せない)
- 番号付けの規則は設計で決めること
- 既存キー操作(カーソル移動・削除)は複数行対応に拡張
### ツール UI フレームワーク
- ツール呼び出し 1 回 = 1 ブロック。`tool_use_id` で同一ブロックに集約
- 各ブロックは以下の状態を遷移する:
- **Pending**: `ToolCallStart` 受信、args 未確定
- **Streaming**: `ToolCallArgsDelta` を連結中。ライブ反映
- **Executing**: `ToolCallDone` 受信、args 確定、結果待ち
- **Done / Error**: `ToolResult` 受信
- **Incomplete**: `ToolResult` が来ないまま turn が終わった場合
- ブロックの見た目は **ツール毎のレンダラ** が決める。ツール名で dispatch、マッチしなければデフォルトレンダラ
- 並列ツール呼び出しは複数ブロックが同時に存在する。`tool_use_id` で振り分け
- Streaming 中の args は生の文字列として連結し、途中の JSON を整形しようとしない(破綻する)
### 組み込みツールのレンダラ
実装対象: Read / Write / Edit / Glob / Grep / default。
- **Read**: 同一 turn 内で連続する Read 呼び出しを **1 ブロックに集約**する。normal / detail とも「読んだファイル数 + ファイルパスのリスト」を表示、中身は出さない。集約中のライブ表示は実行順に最大 3 行のスクロールウィンドウで読んだファイルを下から追加
- **Write**: summary (Created / Overwrote をラベル色で区別) + 書き込まれた content の先頭 5 行。detail では content 全体
- **Edit**: TUI 側 **ファイル content キャッシュ** から該当ファイルを引き、`args.old_string` / `args.new_string` の置換箇所に対して **前後 3 行の unified diff** を赤 / 緑で表示
- **Glob**: `ToolResult.output` の先頭数行をそのまま表示
- **Grep**: 同上
- **default (未知ツール)**: normal で pretty JSON 化した args の先頭 3 行 + `ToolResult.output` の先頭 3 行。detail では全体
### ファイル content キャッシュ
Edit レンダラが diff を出すために TUI 側に持つ。責務は表示用で、ツール実装層の policy (`Tracker`) とは独立。
- Read レンダラが `ToolResult.output` からキャッシュに content を保存
- Write レンダラが `args.content` でキャッシュを更新
- Edit レンダラが `args.old_string` / `args.new_string` でローカル置換してキャッシュを更新
- `Event::History` 再生時も同じ順序でキャッシュを再構築する
### 既存機能の移植
以下は現行 TUI に既にある機能で、新アーキテクチャでも保持する:
- Greeting カード表示
- TurnHeader / UserMessage / AssistantText
- Notification (Warn / Error レベル)
- Compact 開始 / 完了 / 失敗
- ターン終了時の統計 (requests / tokens)
- Ctrl-C 2-tap でのアプリ終了 (Pod 自体は存続)
- shutdown_confirm 挙動
- Paused 状態での空 Enter → Resume
- `Event::History` によるセッション再接続時の履歴復元
### 履歴再生
- `Event::History` を受けたとき、過去のツール呼び出しは **最終状態のブロック** として履歴モデルに組み直される
- tool_call と tool_result の対応付け、および content キャッシュの再構築もこの経路で行う
### イベント欠落耐性
- プロバイダ起因でイベントが欠落したり順序が逆転しても panic しない
- `ToolResult` が来ないまま `TurnEnd` が来た場合、該当ブロックは Incomplete として残す(握りつぶさず視覚化する)
## 設計で決めること
- **キーバインド**: スクロール / ターン移動 / モード切替 のキー割り当て
- **折りたたみの粒度**: 全体トグルのみか、ターン単位の個別開閉も持たせるか
- **Input エリアの高さ上限**: 画面の N% か絶対行数か
- **ペースト番号付け**: TUI 起動中で通しか turn 毎にリセットするか
- **normal モードの圧縮基準**: 5〜6 行は目安。ブロック種別毎の実装上の判断基準
- **末尾追従の解除条件**: 上にスクロールした瞬間に解除するか、数行離れたら解除するか
- **Read 集約の切れ目**: 「連続」の定義(別ツール呼び出しや assistant text が挟まったら切る / `ToolResult` の受信順で切る 等)
## 前提
- `tickets/protocol-tool-result-shape.md`: `Event::ToolResult``summary: String` が追加されていること
## 完了条件
- TUI が alternate screen buffer で起動し、アプリ終了でスクロールバックに何も残らない
- ウィンドウリサイズで separator の増殖が発生しない
- 既存機能 (greeting / turn / user / assistant / tool / notification / compact / stats / ctrl-c / paused / history) がすべて保持されている
- 3 段モード (detail / normal / overview) で履歴の密度を切り替えられる
- Input が複数行 / CJK / ペースト プレースホルダに対応している
- ツール呼び出しが 1 ブロックとして表示され、start → args streaming → done → result の遷移が同じブロックの更新として見える
- 組み込み 5 ツール (Read / Write / Edit / Glob / Grep) がそれぞれ専用レンダラで表示される
- 未知ツールがデフォルトレンダラで表示される
- 並列ツール呼び出しで `tool_use_id` が正しく振り分けられる
- `ToolResult` が欠落したまま turn が終わった場合に該当ブロックが Incomplete として履歴に残り、panic しない
- `Event::History` 再生で過去のツール呼び出しがブロックとして復元され、Edit レンダラ用の content キャッシュも再構築される
## 範囲外
- ツール実行のキャンセル / 介入 UI
- 複数 Pod の spawn / 切替 UI (`tickets/tui-pod-spawn-ui.md`)
- Markdown レンダリング / シンタックスハイライト / リッチ diff レンダリング
- スクロールバック保存(仕様上出さない)
- TUI 独自の履歴永続化(再接続時は Pod 側の `Event::History` に任せる)
- 組み込みツールの `ToolOutput.content` 構造化 (protocol 拡張はスコープ外。必要になったら別チケット)