Skip to content

fix: commandmate wait のエージェント完了検出が機能しない #520

@Kewton

Description

@Kewton

Note: このIssueは 2026-03-18 にレビュー結果(イテレーション4: 影響範囲レビュー2回目)を反映して更新されました。
詳細: dev-reports/issue/520/issue-review/

概要

commandmate wait がエージェントのタスク完了(入力プロンプトに戻った状態)を検出できず、常にタイムアウト(exit 124)になる。

対策: current-output API に sessionStatus フィールドを追加し、wait の完了判定を修正する。

関連: Issue #518(CLI基盤コマンド実装)

現象

エージェントがタスクを完了し入力プロンプト()に戻っても、wait が完了を検出せずタイムアウトする。

$ commandmate send localllm-test-main "ディレクトリ構成を教えて"
Message sent.

$ commandmate wait localllm-test-main --timeout 60
Waiting: localllm-test-main (running=true, prompt=false)
Waiting: localllm-test-main (running=true, prompt=false)
...
Timeout: localllm-test-main exceeded 60s
# exit 124(タイムアウト)

# この時点でエージェントは既に応答完了し ❯ プロンプトで入力待ち

根本原因

wait の完了条件(src/cli/commands/wait.ts:88 - pollWorktree() 内の完了判定)

if (!data.isRunning && !data.isPromptWaiting) {
  return { exitCode: WaitExitCode.SUCCESS };
}

current-output API の isRunning の定義(src/app/api/worktrees/[id]/current-output/route.ts:56 - cliTool.isRunning() 呼び出し)

const running = await cliTool.isRunning(params.id);  // tmuxセッションの生存チェック
// ...
return NextResponse.json({
  isRunning: true,  // tmuxセッションが起動中 = 常にtrue
});

isRunningtmuxセッションの生存 を意味しており、エージェントがタスクを実行中かどうか ではない。

状態と判定の不一致

エージェントの実際の状態 isRunning isPromptWaiting status-detector waitの判定
セッション未起動 false false (N/A) exit 0 ✅
タスク実行中 true false running 待機継続 ✅
入力待ち( true false ready 待機継続 ❌
Y/N確認中 true true waiting exit 10 ✅

既に正しく検出されているが使われていない情報

detectSessionStatus()src/lib/detection/status-detector.ts)は status: 'ready', reason: 'input_prompt' を正しく返している。しかし current-output API の GET ハンドラ(route.ts)のレスポンスにこのステータスが含まれていないため、CLI の waitpollWorktree() 関数)が活用できない。

対策: current-output API に sessionStatus を追加

1. API レスポンスにフィールド追加

src/app/api/worktrees/[id]/current-output/route.ts:

isRunning===true の場合(detectSessionStatus 呼び出し後)

return NextResponse.json({
  isRunning: true,
  sessionStatus: statusResult.status,          // 追加: 'ready'|'running'|'waiting'
  sessionStatusReason: statusResult.reason,    // 追加: 'input_prompt'|'thinking_indicator'|'default' 等
  // ...既存フィールドそのまま(isRunning, isPromptWaiting 等は変更しない)
});

isRunning===false の早期リターンの場合(route.ts:58-66)

return NextResponse.json({
  isRunning: false,
  sessionStatus: 'idle' as const,              // 追加: セッション未起動時は 'idle'
  sessionStatusReason: 'session_not_running',  // 追加: 早期リターンの理由を明示
  // ...既存フィールドそのまま
});

補足: sessionStatus: 'idle'isRunning===false の早期リターン時にのみ設定される値であり、detectSessionStatus() の戻り値ではない。detectSessionStatus()'ready'|'running'|'waiting' のみを返す。

既存UIへの影響なし: フィールド追加のみで、既存フィールドは変更しない。ブラウザUIは sessionStatus を無視するだけ。

2. wait の完了条件を変更

src/cli/commands/wait.ts:

// Before
if (!data.isRunning && !data.isPromptWaiting) {
  return { exitCode: WaitExitCode.SUCCESS };
}

// After
if (!data.isRunning || data.sessionStatus === 'ready') {
  return { exitCode: WaitExitCode.SUCCESS };
}

sessionStatus が undefined の場合のフォールバック

本 Issue 適用後の新しいサーバーでは、isRunning===false 時に sessionStatus: 'idle' が返されるため undefined にはならない。sessionStatusundefined となるのは、本 Issue 適用前の古いサーバーに CLI が接続した場合のみである。

古いバージョンのサーバーが稼働中の場合、sessionStatus フィールドがレスポンスに含まれないことがある。この場合:

  • sessionStatusundefined となり 'ready' とは一致しない
  • !data.isRunning による従来の完了検出パスは引き続き機能する
  • つまり、古いサーバーでは本Issueの修正前と同じ挙動(isRunning=false のみで完了検出)にフォールバックする

実装時のコメント例:

// sessionStatus === 'ready': エージェントがタスク完了(入力プロンプトに戻った)
// sessionStatus === undefined: 古いサーバーとの互換性のため、isRunning のみで判定
//   (新サーバーでは isRunning===false 時に sessionStatus='idle' が返るため undefined にならない)
if (!data.isRunning || data.sessionStatus === 'ready') {
  return { exitCode: WaitExitCode.SUCCESS };
}

プロンプト検出との判定フロー

wait のポーリングループでは以下の順序で判定が行われる:

  1. プロンプト検出チェック(既存ロジック): data.isPromptWaiting === true && data.promptData の場合 → exit 10(Y/N確認プロンプト)
  2. 完了条件チェック(本Issue修正): !data.isRunning || data.sessionStatus === 'ready' の場合 → exit 0(タスク完了)
  3. 上記いずれでもない場合 → 待機継続

この順序により:

  • Y/N 確認プロンプト: hasActivePrompt=trueisPromptWaiting=true → ステップ1で exit 10
  • 入力プロンプト(等): hasActivePrompt=falseisPromptWaiting=falsesessionStatus='ready' → ステップ2で exit 0
  • エージェント実行中: isPromptWaiting=falsesessionStatus='running' → 待機継続

time-based heuristic(no_recent_output)に関する注意

status-detector.ts:404-416detectSessionStatus() 内の時間ベース判定ロジック)では、最後の出力から5秒以上経過すると confidence: 'low'status: 'ready', reason: 'no_recent_output' を返す。エージェントがLLM推論中(出力なし)の場合にも ready と判定される可能性がある。

本Issueでの対応方針: no_recent_output による ready 判定は現行の status-detector の仕様として受け入れる。理由:

  • 実際のユースケースでは、LLM推論が5秒を超えても最終的にはストリーミング出力が始まるため、誤検出は一時的
  • --stall-timeout オプションにより、ユーザーが出力停滞に対する独自のタイムアウトを設定可能
  • 将来的に精度向上が必要な場合は、sessionStatusConfidence フィールドの追加(F1-05参照)や no_recent_output 除外条件の導入を検討する

3. CLI 型定義の更新

src/cli/types/api-responses.tsCurrentOutputResponse に追加:

export interface CurrentOutputResponse {
  isRunning: boolean;
  sessionStatus?: 'idle' | 'ready' | 'running' | 'waiting';  // 追加
  sessionStatusReason?: string;                                 // 追加
  thinking: boolean;                                            // 型修正: string → boolean(API実装に合わせる)
  // ...既存フィールド
}

補足: thinking フィールドの型修正について:

  • これは cli/types/api-responses.ts の型定義のみの修正であり、サーバー側 route.ts の挙動は変更しない(既に boolean を返している)
  • 既存テスト tests/unit/cli/commands/wait.test.tsbaseOutput.thinking''(空文字列)から false に修正する必要がある

4. ls コマンドの STATUS 列改善(任意)

src/cli/commands/ls.ts の STATUS 列を sessionStatus で表示:

ID                      NAME      STATUS   AGENT
localllm-test-main      main      ready    claude    ← sessionStatus を反映
mycodebranchdesk-main   main      idle     claude

注意: lsderiveStatus() は worktree 一覧 API のフラグ(isWaitingForResponse, isProcessing, isSessionRunning)から導出しており、current-output API の sessionStatus とは別の情報源を使っている。将来的に worktree 一覧 API にも sessionStatus を統一して含める方針を検討する。

受け入れ条件

  • current-output API のレスポンスに sessionStatus フィールドが含まれる
  • current-output API のレスポンスに sessionStatusReason フィールドが含まれる
  • isRunning===false の早期リターン時に sessionStatus: 'idle', sessionStatusReason: 'session_not_running' が返される
  • commandmate wait がエージェントのタスク完了(sessionStatus === 'ready')を検出し exit 0 で返る
  • エージェント実行中(sessionStatus === 'running')は待機継続される
  • プロンプト検出(isPromptWaiting === true)は従来通り exit 10 で返り、sessionStatus === 'ready' の完了条件より先に評価される
  • セッション未起動(isRunning === false)は従来通り exit 0 で返る
  • sessionStatusundefined(古いサーバー)の場合、isRunning のみによる従来の完了検出にフォールバックする
  • no_recent_output による ready 判定は現行仕様として許容する(テストで挙動を明示)
  • 既存ブラウザUIに影響がない
  • thinking フィールドの型を boolean に修正する(api-responses.ts)
  • 単体テストで以下の完了検出パスをカバー:
    • isRunning=true, sessionStatus='ready' → exit 0(タスク完了)
    • isRunning=true, sessionStatus='running' → 待機継続
    • isRunning=true, sessionStatus='waiting', isPromptWaiting=false → 待機継続(selection list等)
    • isRunning=true, isPromptWaiting=true → exit 10(sessionStatus より先に評価)
    • sessionStatus=undefined(古いサーバー互換)→ isRunning のみでフォールバック
    • isRunning=true, sessionStatus='ready', reason='no_recent_output' → exit 0(低信頼度での完了許容を明示)
  • --stall-timeoutsessionStatus の相互作用テスト: ポーリング1回目で sessionStatus='running'(content変化なし)、2回目で sessionStatus='ready' の場合、stall-timeout(例: 8秒)内であれば exit 0 が返ること
  • 既存テスト wait.test.tsbaseOutput.thinking'' から false に修正
  • 複数 worktree 同時待機時の sessionStatus 混在については本 Issue のスコープ外とし、必要に応じて別 Issue で対応する(例: worktree-A が sessionStatus='ready' で即完了、worktree-B が2回ポーリング後に完了するケース)

影響範囲

変更が必要なファイル

ファイル 変更内容
src/app/api/worktrees/[id]/current-output/route.ts sessionStatus, sessionStatusReason フィールド追加(isRunning===false パスにも追加)
src/cli/commands/wait.ts 完了条件変更(sessionStatus === 'ready')、sessionStatus===undefined フォールバックコメント追加
src/cli/types/api-responses.ts CurrentOutputResponse に型追加、thinking 型修正(stringboolean
src/cli/commands/ls.ts STATUS列の表示ロジック改善(任意)
tests/unit/cli/commands/wait.test.ts 完了検出テスト追加、baseOutput.thinking''false に修正。注意: baseOutputisRunning=false ケースは、サーバー側の早期リターンレスポンス(isRunning, content, lineCount, cliToolId のみ)と形状が異なる。wait.tspollWorktree() 内で CurrentOutputResponse 型にキャストされた data を参照するため、テスト側では完全な型を提供する必要があるが、存在しないフィールドは undefined となる点に留意する(これは既存の問題であり本 Issue の範囲外)
src/cli/commands/capture.ts コード変更不要。capture --json 出力に sessionStatus/sessionStatusReason が新たに含まれる(後方互換、追加フィールドのみ)

変更不要だが確認が必要なファイル

ファイル 確認内容
src/components/worktree/WorktreeDetailRefactored.tsx 変更不要。current-output API を fetch しているが、sessionStatus フィールドは使用しない。追加フィールドは無視される。UI側の CurrentOutputResponse インターフェース(116行目)に sessionStatus?/sessionStatusReason? のオプショナルフィールド追加を推奨(現時点では使用しないが、将来のUI連携に備える)
tests/integration/current-output-thinking.test.ts sessionStatus フィールドのアサーション追加を検討。既存アサーションは影響を受けないが、新フィールドの存在確認を追加することが望ましい

影響を受けないが関連するモジュール

以下のモジュールは detectSessionStatus() を直接呼び出す、または sessionStatus の概念に関連するが、今回の変更では修正不要:

モジュール 理由
src/lib/polling/response-poller.ts サーバーサイドで detectSessionStatus を直接呼び出しており、API経由ではないため影響なし
src/lib/polling/auto-yes-manager.ts 同上。Auto-Yesポーリングは独立したサーバーサイドロジック
src/lib/session/worktree-status-helper.ts Worktreeステータス一括検出。detectSessionStatus を直接利用するが、API変更の影響は受けない
src/lib/detection/status-detector.ts detectSessionStatus() の実装元。今回は呼び出し元(route.ts)での結果公開のみであり、検出ロジック自体は変更しない

レビュー履歴

イテレーション 1 (2026-03-18) - 通常レビュー

  • F1-01 (must_fix): isRunning===false 早期リターンに sessionStatus: 'idle', sessionStatusReason: 'session_not_running' を追加。API実装コード例・受け入れ条件に反映。
  • F1-02 (should_fix): sessionStatus: 'idle'detectSessionStatus() の戻り値ではなく早期リターン専用であることを補足説明として明記。
  • F1-03 (should_fix): no_recent_output による ready 誤検出リスクを「time-based heuristic に関する注意」セクションとして追加。対応方針と将来的な改善案を記載。受け入れ条件にも反映。
  • F1-04 (should_fix): プロンプト検出と sessionStatus=ready の判定フローを詳細に記載。受け入れ条件で評価順序を明示。
  • F1-05 (nice_to_have): sessionStatusConfidence フィールドの将来的な追加可能性をF1-03の注意セクション内で言及。
  • F1-06 (nice_to_have): lsderiveStatus()sessionStatus の情報源の違いを注意書きとして追記。
  • F1-07 (nice_to_have): thinking フィールドの型不一致(stringboolean)を型定義セクション・受け入れ条件に反映。

イテレーション 2 (2026-03-18) - 影響範囲レビュー

  • F3-03 (must_fix): thinking フィールドの型変更に伴い、既存テスト wait.test.tsbaseOutput.thinking 修正が必要であることを明記。型定義のみの修正でありサーバー側変更なしであることを補足。影響範囲テーブルにテストファイルの修正内容を追記。受け入れ条件にテスト修正項目を追加。
  • F3-05 (must_fix): sessionStatusundefined(古いサーバー)の場合のフォールバック動作を明文化。wait.ts の実装コメント例を追加。受け入れ条件にフォールバック項目を追加。テストケースに sessionStatus=undefined のケースを追加。
  • F3-01 (should_fix): capture.ts を影響範囲テーブルに追加(コード変更不要、出力に新フィールドが追加される旨を注記)。
  • F3-02 (should_fix): WorktreeDetailRefactored.tsx を「変更不要だが確認が必要なファイル」テーブルに追加。
  • F3-04 (should_fix): 受け入れ条件のテスト項目を具体的なテストケース6件に展開(ready/running/waiting/prompt優先/undefined互換/no_recent_output)。
  • F3-06 (should_fix): 「影響を受けないが関連するモジュール」テーブルを新設し、response-poller.ts, auto-yes-manager.ts, worktree-status-helper.ts, status-detector.ts を列挙。
  • F3-08 (should_fix): tests/integration/current-output-thinking.test.ts を「変更不要だが確認が必要なファイル」テーブルに追加。
  • F3-10 (should_fix): --stall-timeoutsessionStatus の相互作用テストケースを受け入れ条件に追加。
  • F3-07 (nice_to_have): スキップ。CLAUDE.md のモジュールリファレンス更新は実装時に対応。
  • F3-09 (nice_to_have): スキップ。ls の STATUS 列に関する注意書きは既に記載済み。

イテレーション 3 (2026-03-18) - 通常レビュー(2回目)

  • F5-01 (should_fix): フォールバックセクションに「新しいサーバーでは isRunning===false 時に sessionStatus: 'idle' が返されるため undefined にはならない。undefined となるのは本 Issue 適用前の古いサーバーに接続した場合のみ」と補足。実装コメント例にも反映。
  • F5-02 (nice_to_have): 根本原因セクションのコード参照に関数名・変数名を併記(pollWorktree(), cliTool.isRunning(), detectSessionStatus() 等)。行番号のみの参照を堅牢化。

イテレーション 4 (2026-03-18) - 影響範囲レビュー(2回目)

  • F7-01 (should_fix): 「変更不要だが確認が必要なファイル」テーブルの WorktreeDetailRefactored.tsx 行に、UI側 CurrentOutputResponse インターフェースへの sessionStatus?/sessionStatusReason? オプショナルフィールド追加推奨を追記(将来のUI連携に備える)。
  • F7-02 (should_fix): 「変更が必要なファイル」テーブルの wait.test.ts 行に、baseOutputisRunning=false ケースがサーバー側の早期リターンレスポンスと形状が異なる点に関する注意書きを追記(既存の問題であり本 Issue の範囲外)。
  • F7-04 (should_fix): 複数 worktree 同時待機時の sessionStatus 混在ケースについて、受け入れ条件に「本 Issue のスコープ外とし、必要に応じて別 Issue で対応する」旨の項目を追加。
  • F7-03 (nice_to_have): スキップ。capture --json の formatJson フィールド出力順序は機能上の問題なし。Issue 本文の記載は正確。
  • F7-05 (nice_to_have): スキップ。エラーレスポンス時の sessionStatus 有無は既存の設計であり、本 Issue の範囲外。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions