diff --git a/TODO.md b/TODO.md index e697d0a0..b8888ffe 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,8 @@ - Agent Skills を Workflow として ingest → [tickets/agent-skills.md](tickets/agent-skills.md) - パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) - Pod CLI: マニフェスト関連フラグの整理 → [tickets/pod-cli-manifest-flags.md](tickets/pod-cli-manifest-flags.md) +- Pod: 空応答ターン (Submit 後 AI 応答ゼロで Pause/Cancel) を自動巻き戻し → [tickets/pod-empty-turn-rollback.md](tickets/pod-empty-turn-rollback.md) +- Pod: 任意ターンからの Fork(複数ターン巻き戻しを汎用化) → [tickets/pod-session-fork.md](tickets/pod-session-fork.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) @@ -11,6 +13,8 @@ - Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md) - ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md) - spawn 失敗時に Pod の stderr が TUI に表示されない → [tickets/tui-spawn-error-surface.md](tickets/tui-spawn-error-surface.md) + - role:system の system message を TUI に表示する仕組み → [tickets/tui-system-message-render.md](tickets/tui-system-message-render.md) + - 巻き戻されたターンの入力テキストを編集領域に復元 → [tickets/tui-empty-turn-restore.md](tickets/tui-empty-turn-restore.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) diff --git a/tickets/pod-empty-turn-rollback.md b/tickets/pod-empty-turn-rollback.md new file mode 100644 index 00000000..c813d16b --- /dev/null +++ b/tickets/pod-empty-turn-rollback.md @@ -0,0 +1,43 @@ +# Pod: 空応答ターンの自動巻き戻し + +## 背景 + +`Method::Run` でユーザーが Submit したあと、AI 応答が一切 history に積まれないまま Pause / Cancel されると、現状の Pod は以下の状態を確定させる: + +- `worker.history` に user message と attachment 由来の system message が残る +- `pod.user_segments` に当該入力が push 済み +- `session_store` 側にも `save_user_input` の delta が commit されており、続いて `save_delta`(実体は空 or attachments のみ)/`save_turn_end`/`save_run_completed(interrupted=true)` が積まれて head_hash が前進する + +結果として「ユーザー発話だけがあり、AI 応答ゼロ」のターンが履歴に commit され、次の Run はその上に積み増される。Submit を取り消して書き直す・別の話に切り替える、といった操作のたびにノイズが残ってしまう。 + +人間操作の TUI ではこのケースが頻発するので、Submit 前の状態に丸ごと巻き戻す仕組みがほしい。 + +## 要件 + +- Pod controller のターン処理で、`Method::Run` 起点のターンが以下を **両方** 満たす場合は Submit 直前の状態に巻き戻す: + - 終了が Pause / Cancel 由来である(`Method::Pause` / `Method::Cancel` を受けて `WorkerError::Cancelled` で抜けた経路) + - そのターンで `worker.history` に LLM 応答に由来する entry(assistant message / tool_use / tool_result)が **一つも** append されていない +- 巻き戻しは Submit 起点で生まれた変更を全て取り消す: + - `worker.history` を Submit 前の長さに truncate + - `pod.user_segments` から push した分を pop + - `pending_attachments` を破棄 + - `session_store` の head_hash を Submit 直前まで戻し、`save_user_input` / `save_delta` / `save_turn_end` / `save_run_completed` の commit を相殺 + - `runtime_dir.write_history` / `write_status` で永続化済みの `history.json` / `status` を同期 +- 巻き戻し成立時の最終 status は **Idle**(Resume すべき AI ターンは存在しないため Paused にしない) +- 巻き戻しの有無はクライアントが判別できるようイベントで伝える(`Event::RunEnd` の variant 拡張、または専用イベント)。これにより TUI 側で「巻き戻されたので入力欄に戻した」等のフィードバックが組める。 +- 対象は `Method::Run` 起点のターンのみ。Notify 起点の自動 Run(`run_for_notification`)と `Method::Resume` は対象外。 +- Mid-run compaction 後の resume で LLM 応答ゼロになるケース(`do_compact_and_resume` 経路)は、Submit 前の history snapshot が依然有効である限り同様に巻き戻せる設計とする。 + +## 完了条件 + +- TUI / pod_cli いずれの経路でも、`Method::Run` → AI 応答ゼロで Pause / Cancel すると Pod の in-memory 状態(`worker.history`, `user_segments`, `pending_attachments`, status)が Submit 前と一致する +- 同条件で session_store / `history.json` / `status` の永続側も Submit 前と一致する +- AI 応答が 1 件でも積まれていたターンは従来通り、巻き戻さずに Paused / Idle で確定する +- クライアントが受け取るイベントから巻き戻しの有無が分かる +- ユニットテストで「assistant 応答ゼロでの Cancel」「assistant 応答ゼロでの Pause」「tool_use 1 回のあとの Cancel(巻き戻さない)」「Notify 起点の Cancel(対象外)」の 4 ケースを最低限カバーする + +## 範囲外 + +- 複数ターンに遡る undo(→ `tickets/pod-session-fork.md` で Fork として実装する計画) +- ユーザーの明示操作で「このターンを巻き戻す」を選ばせる UI(自動条件のみ) +- TUI 側で「巻き戻された入力を編集領域に復元する」等の UX(→ `tickets/tui-empty-turn-restore.md`) diff --git a/tickets/pod-session-fork.md b/tickets/pod-session-fork.md new file mode 100644 index 00000000..0021b9af --- /dev/null +++ b/tickets/pod-session-fork.md @@ -0,0 +1,56 @@ +# Pod: 任意ターンからの Fork(複数ターン巻き戻し) + +## 背景 + +`tickets/pod-empty-turn-rollback.md` は「直近 Submit が AI 応答ゼロのまま中断された」極めて狭いケースだけを自動で巻き戻す簡易フォーム。それを超える「3 ターン前から別の方針でやり直したい」「ある分岐は捨てて別ルートを試したい」といった **複数ターン巻き戻し** は、過去ターン境界からの Fork として実装する。 + +session_store には既に primitive が揃っている: + +- `session::fork(state)` — 現状から新 session_id へ分岐(`crates/session-store/src/session.rs:400`) +- `session::fork_at(source_id, at_hash)` — 既存セッションログ上の任意 entry hash から分岐(同 :424) +- `SessionOrigin { session_id, at_hash }` — `SessionStart.forked_from` に出自を記録 + +未着手なのは Pod / protocol / クライアントへの露出と、ターン境界 ↔ entry hash の対応付け。 + +## 要件 + +- Pod に「現セッションから Fork して新セッションへ切り替える」操作を追加。Fork 起点はターン境界で指定する: + - protocol に新 Method(仮: `Method::Fork { from: ForkPoint }`)を追加 + - `ForkPoint` は最低限「ターン番号」「entry hash」のいずれかで起点を指す + - ターン番号 → entry hash の解決は Pod / session_store 側で行う(`save_turn_end` のログ entry を境界として使うのが自然) +- Fork 後の Pod 状態: + - 新 session_id を active に切り替え、worker.history を fork 起点までの内容で再構築 + - 元セッションは破壊されない(後から switch back 可能な前提を残す) + - 走行中(Running / Paused)状態での Fork は拒否し、Idle 限定 +- Fork ツリーが追跡可能であること: + - `forked_from` chain が session_store のログから辿れる(既存挙動の確認込み) + - クライアントから「このセッションの祖先 / 子孫」を引ける API(最低限、`SessionOrigin` を読める形) +- pod_cli / TUI からの呼び出しインターフェースの設計: + - 本チケットで protocol 上の Method は確定させる + - pod_cli の引数仕様もここに含める(最低限 `pod fork --turn N` 程度) + - TUI 側の UX(ターンを選択して fork する操作)は別チケット +- セッション切り替え後の `runtime_dir` の扱いを decide: + - 1 つの runtime に対して active session が切り替わる形 + - セッションごとに別 runtime を持つ形 + - のどちらが今の構成と整合するか調査の上で決定 + +## 完了条件 + +- `Method::Fork` で過去ターン起点の新セッションが作成され、そこから `Method::Run` で続行できる +- 元セッションが fork 後も独立に存在し、別 Pod プロセスから resume できる(破壊されていない) +- Fork ツリーが session_store のログから機械的に辿れる +- pod_cli から fork → 新セッションでの run まで通せる +- `pod-empty-turn-rollback` の自動巻き戻しと共存(自動巻き戻しは本機能を使わずに従来通りの直接 truncate 方式で良い、と決めるならその根拠も明記) + +## 範囲外 + +- Fork ツリーの可視化 UI(TUI / GUI)— 別チケット +- TUI 上での「ターンを選んで fork」UX — 別チケット +- 異なる Fork 間でのマージ +- 過去ターンの物理削除型リベース(fork は常に非破壊) +- 自動 GC / 古い fork の整理 + +## 依存 / 関連 + +- `tickets/pod-empty-turn-rollback.md`(直近 Submit のみの自動巻き戻し。本チケットは汎用 fork) +- session_store の既存 `fork` / `fork_at` を流用 diff --git a/tickets/tui-empty-turn-restore.md b/tickets/tui-empty-turn-restore.md new file mode 100644 index 00000000..241c4d03 --- /dev/null +++ b/tickets/tui-empty-turn-restore.md @@ -0,0 +1,35 @@ +# TUI: 巻き戻された入力の編集領域への復元 + +## 背景 + +`tickets/pod-empty-turn-rollback.md` で Pod 側に「Submit 後 AI 応答ゼロで Pause/Cancel した場合に Submit 前の状態へ自動巻き戻す」仕組みを入れる。Pod は巻き戻しの有無を `Event::RunEnd`(または専用イベント)で TUI に通知する。 + +TUI 側は現状 `submit_input` 時点で `input.clear()` を呼んでおり、Submit 後はテキストが失われる。Pod が巻き戻したのに TUI 側だけ「空入力欄 + ターンヘッダーが画面に残った状態」では UX が破綻する。Pod の巻き戻しに合わせて、TUI も Submit 前と同等の見た目に戻す。 + +## 要件 + +- Pod から巻き戻し通知を受け取った場合: + - 直近 Submit でレンダリングしたブロック(`Block::TurnHeader`, `Block::UserMessage`, それに付随する `[File: ...]` 等の attachment ブロック、`Block::TurnStats` 等の Run 由来ブロック全般)を画面から取り除く + - 入力欄に当該 Submit のテキスト(typed segments)を復元し、カーソル位置はテキスト末尾 + - `running` / `paused` / `assistant_streaming` / `current_tool` / `run_*_tokens` 等のターン状態は Idle 相当にリセット +- 復元は 1 ターン分のみ(複数巻き戻しは Pod 側でも対象外) +- 入力欄に未送信の文字(Run 中にユーザーが書き始めたもの、tui-input-queue が入ったらキュー内容)が既にある場合の振る舞いを決める: + - 既入力を上書きしない方針が素直(巻き戻し復元分は別バッファに置いて「↑キーで呼び出し」等にする手もある) + - tui-input-queue とのバッティングはチケット着手時に再確認 +- 巻き戻された旨をユーザーに 1 行のヒント(status line / トースト等)で出す。サイレントに戻すと「画面が消えて何が起きたか分からない」事故になりやすい + +## 完了条件 + +- TUI で Submit → AI 応答が始まる前に Pause/Cancel すると、画面が Submit 前と一致し、入力欄に元のテキストが戻っている +- AI が 1 トークンでも応答した後の Pause/Cancel では従来通り(巻き戻さず Paused / Idle) +- 巻き戻しが起きたことがユーザーから視認できる + +## 範囲外 + +- 複数ターン巻き戻し(→ `tickets/pod-session-fork.md` で Fork として実装する計画) +- 巻き戻し履歴のスタック化(直前 1 件のみ復元) +- pod_cli / 他クライアントの同等 UX(本チケットは TUI 限定) + +## 依存 + +- `tickets/pod-empty-turn-rollback.md` の Pod 側実装 + 巻き戻し通知イベントの確定 diff --git a/tickets/tui-system-message-render.md b/tickets/tui-system-message-render.md new file mode 100644 index 00000000..979843bb --- /dev/null +++ b/tickets/tui-system-message-render.md @@ -0,0 +1,47 @@ +# TUI で role:system の system message を表示する + +## 背景 + +Pod は user 入力の `@` chip / `/` chip を submit 時に展開し、`Item::system_message` を `pending_attachments` 経由で worker.history に commit している: + +- `@` ライブ submit: `Pod::run` → `resolve_file_refs` (crates/pod/src/pod.rs:825,852) → `PodFsView::resolve_file_ref` (crates/pod/src/fs_view.rs:119) で `[File: ]\n` を生成 +- compact worker の auto-read: `mark_read_required` で nominate された再読対象が `PodFsView::render_auto_read` (crates/pod/src/fs_view.rs:84) で `[Auto-read file: :]\n` として乗る +- `/` ライブ submit: `Pod::run` → `resolve_workflow_invocations` (crates/pod/src/pod.rs:826,876) → `crate::workflow::resolve_workflow_invocation` (crates/pod/src/workflow/mod.rs:74) で `[Workflow /]\n` と requires Knowledge 毎の `[Workflow / requires Knowledge #]\n` を生成 + +いずれも `PodInterceptor::on_prompt_submit` (crates/pod/src/ipc/interceptor.rs:114) で `PromptAction::ContinueWith(extras)` 経由で worker.history に commit され、history.json に永続化されている (CLAUDE.md「許される加工」原則に整合)。 + +## 問題 + +LLM のコンテキストには乗っているが TUI には何も出ない。TUI 側で role:system の Item が表示経路に乗っていないため: + +1. **ライブ側**: 解決済み system message を運ぶ broadcast event が `protocol::Event` に存在しない (crates/protocol/src/lib.rs:204–326)。`UserMessage` で `@` / `/` chip 自体は表示されるが、解決された本体は出ない。失敗時のみ `Alert` で出るが、`Alert` はユーザー向け一過性通知で永続化されない (crates/protocol/src/lib.rs:328–348) ため表示経路として不適切。 +2. **履歴復元側**: `App::restore_history` (crates/tui/src/app.rs:650–702) の match が `role: "user"` / `"assistant"` 以外を `_ => {}` で握り潰す。history.json に system role で残っているのに resume 時に消える。 + +結果として「`@` や `/` を submit したのに、本当に読まれたのか / 何が context に乗ったのか TUI からは判別できない」状態になっている。 + +## 要件 + +- `Item::system_message` (role:system) を user / assistant メッセージと並列のログ要素として TUI に表示する**一般的な仕組み**を入れる。種別ごとの個別パッチではなく、role:system が来たら一律で表示経路に乗せる形にする。 +- 仕組みとして最小限カバーすべき system message: + - `[File: ]` (ライブ `@`) + - `[Auto-read file: :]` (compact 後の auto-read) + - `[Workflow /]` + - `[Workflow / requires Knowledge #]` +- 表示の単一の情報源は永続化された history (= history.json の role:system Item)。ライブ submit 時 / 履歴復元時 / 別 client subscribe 時 の三経路で同じ Block バリアントを通る。 +- 表示は本文プレビュー(数行 + 残行数 / 切り詰め注記)程度で良い。`[Auto-read file: ...:]` の range ラベルは可視化する。workflow 本体と requires Knowledge は同じ workflow 起動に紐づく一連と分かる粒度で識別できる。 +- 解決失敗時の従来経路は維持: `@` は `Alert`、workflow は user-invocation エラーとして即座に Pod から返る (`Pod::validate_workflow_invocations`)。 + +## 範囲外 / 非目標 + +- `` 注入機構そのものの汎用化や、notify_wrapper 適用後の本文表示。これらは別所(`session-todo-reminder` 等)。本チケットは**表示側の仕組みのみ**で、将来 `` 系が role:system Item として history に乗るようになれば、同じ表示経路にそのまま流れる前提。 +- 表示形式の凝った装飾(シンタックスハイライト / 折り畳み UI)。最初は素のテキストプレビューで十分。 +- `model_invokation: true` のみの workflow(user_invocable=false)の表示は対象外。 + +## 完了条件 + +- `@` を submit すると、user message ブロックに続けて自動読み取り結果が TUI に出る。本文プレビューが視認できる。 +- `/` を submit すると、workflow 起動結果のログ要素が TUI に出る。`requires` がある場合は Knowledge 注入もそれと分かる形で出る。 +- compact 後の resume で `[Auto-read file: ...]` が同じログ要素として復元・表示される。 +- 別 client が後から subscribe して `Event::History` を受けた場合も、同じログ要素として描画される。 +- ライブ event と history 復元の表示が一致する(同じ Block バリアントを通る)。 +- 解決失敗時の従来経路(`Alert` / user-invocation エラー)は維持される。