Skip to content

fix: インタラクティブプロンプト検出時にtmuxバッファ全体がレスポンスとして保存される #326

@Kewton

Description

@Kewton

Note: このIssueは 2026-02-20 にStage 7レビュー結果を反映して更新されました。
詳細: dev-reports/issue/326/issue-review/

概要

response-poller.tsextractResponse()関数で、インタラクティブプロンプト(選択肢形式の質問など)が検出された場合、lastCapturedLineを無視してtmuxバッファ全体をレスポンスとして返している。これにより、前の会話の内容がAssistantメッセージに混入し、History画面で正しいメッセージが表示されない。

再現手順

  1. worktreeのClaude CLIセッションで複数の会話を実行する
  2. tmuxバッファに前の会話の出力が蓄積された状態で新しいコマンド(例: /issue-enhance #323)を送信する
  3. Claudeがインタラクティブプロンプト(選択肢形式の質問)を表示する
  4. History画面でAssistantの応答メッセージを確認する

期待される動作

History画面のAssistantメッセージには、現在の会話(/issue-enhance #323)の応答内容(Phase 1-3の出力)のみが表示される。

実際の動作

History画面のAssistantメッセージに前の会話の内容(例: publish.ymlの修正内容)が表示され、現在の会話の応答が正しく表示されない。

原因

src/lib/response-poller.tsextractResponse()関数に2か所の問題がある:

箇所1: 行326-341(Claude早期プロンプト検出 / "Early check for Claude permission prompts")

if (cliToolId === 'claude') {
    const fullOutput = lines.join('\n');  // ← tmuxバッファ全体
    const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
    if (promptDetection.isPrompt) {
      return {
        response: stripAnsi(fullOutput),  // ← バッファ全体を返している(stripAnsi適用あり)
        isComplete: true,
        lineCount: totalLines,
      };
    }
}

箇所2: 行487-499(フォールバックプロンプト検出 / "Final prompt detection fallback")

const fullOutput = lines.join('\n');  // ← tmuxバッファ全体
const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
if (promptDetection.isPrompt) {
    return {
      response: fullOutput,  // ← バッファ全体を返している(stripAnsi未適用)
      isComplete: true,
      lineCount: totalLines,
    };
}

補足(SF-1): 箇所1ではstripAnsi(fullOutput)が適用されているのに対し、箇所2ではstripAnsiが適用されていない。修正時には箇所2にもstripAnsiを適用するか、または適用しない理由を判断する必要がある。stripAnsi未適用のままだとANSIエスケープコードがDBに保存される可能性がある。

通常レスポンスとの違い

通常のレスポンス抽出(行360-414)ではlastCapturedLineを起点として新しい行のみを抽出しているが、プロンプト検出時のみこのロジックが適用されず、バッファ全体が使われている。

修正方針

extractResponse()のプロンプト検出部分で、通常レスポンスと同様にlastCapturedLine以降の行のみをレスポンスとして返すよう修正する。プロンプト検出自体はバッファ全体で行い(検出精度を維持)、返却するcontent部分のみlastCapturedLine以降に限定する。

startIndex決定ロジックの方針(SF-3)

通常レスポンス抽出(行364-386)のstartIndex決定には以下の4分岐がある:

  1. bufferWasReset - バッファリセット検出時(startIndex = 0)
  2. codex - Codex CLIの場合
  3. lastCapturedLine >= totalLines - 5 - バッファスクロール考慮
  4. 通常 - lastCapturedLineをそのまま使用

プロンプト検出時のレスポンス切り出しにおいても、これらの分岐(特にbufferWasResetやバッファスクロール考慮)を適用するかどうかを検討する必要がある。具体的には:

  • 方針A: 通常レスポンスと同一のstartIndex決定ロジックを再利用する(既存のfindRecentUserPromptIndex等を含む)
  • 方針B: プロンプト検出時は簡略版としてlastCapturedLineのみを使用し、bufferWasResetの場合のみ0にフォールバックする

実装時に両方針のトレードオフを評価し、適切な方を選択すること。

checkForResponse内のpromptDetection再検出への影響(SF-2)

checkForResponse()(行604-627付近)では、extractResponse()の結果に対して再度detectPromptWithOptions(result.response, cliToolId)を実行している。修正後はresult.responselastCapturedLine以降の部分出力になるため、以下の点を確認する必要がある:

  • detectPromptWithOptionsがこの部分出力でもプロンプトを正しく検出できること
  • 特に、lastCapturedLineが質問文の途中にある場合(質問行が以前の会話末尾付近に位置するエッジケース)に、質問文・選択肢が部分出力に含まれる前提が妥当であること
  • プロンプト検出に必要なコンテキスト(質問文・選択肢)は通常、現在の応答の末尾に表示されるためlastCapturedLine以降に含まれるはずだが、バッファスクロールで分断される可能性を考慮すること
  • rawContentへの影響: 修正後、detectPromptWithOptionsに渡される入力がlastCapturedLine以降に縮小するため、prompt-detector.ts内のtruncateRawContent(output.trim())(行583)が生成するrawContentもバッファ全体ではなくlastCapturedLine以降のみとなる。これはIssue #235の設計意図(「完全なプロンプト出力をDB保存」)との整合性を確認する必要がある。通常、プロンプトの質問文+選択肢は20行以内であるため、lastCapturedLine以降に含まれるが、rawContentが意図より短くなるエッジケースがないか注意すること

テスト戦略

extractResponse()は非exportの内部関数であるため、直接のユニットテストが困難である。以下のいずれかの戦略を採用すること:

  • (A) ヘルパー関数抽出: startIndex決定ロジックをヘルパー関数として抽出・exportし、ユニットテストを作成する
  • (B) 結合テスト: checkForResponse()レベルの結合テスト(DB/tmuxモック付き)でプロンプト検出パスを検証する
  • (C) テスト用export: extractResponseをテスト用にexportする(@internalアノテーション付き)

推奨は**(A)**。startIndex決定ロジックを独立関数に切り出すことで、テスト容易性とコードの可読性の両方が向上する。

影響範囲

直接影響

  • src/lib/response-poller.ts - extractResponse()関数の2か所(箇所1: Claude早期プロンプト検出、箇所2: フォールバックプロンプト検出)
  • src/lib/response-poller.ts - checkForResponse()内のpromptDetection再検出(行605付近)。result.responselastCapturedLine以降に縮小するため、再検出の入力が変わる
  • History画面のAssistantメッセージ表示
  • Claudeが選択肢形式の質問を表示するすべてのケース

間接影響

  • src/lib/auto-yes-manager.ts - pollAutoYes()(行529付近)はtmuxバッファを独自にキャプチャしてdetectPrompt()を直接呼び出しており、extractResponse()を経由しないためコード変更は不要。また、Auto-YesのlastAnsweredPromptKey(Issue fix: Auto-Yes Pollerの重複応答によりtmuxセッションが定期的に削除される #306)による重複判定ロジックにも影響しない。generatePromptKey()prompt-key.ts 行37-38)はpromptData.type:promptData.question形式でキーを生成しており、DB保存されるcontentrawContent || cleanContent)とは独立している。pollAutoYes()は独自にtmuxバッファ全体をdetectPrompt()に渡すため、promptData.questionresponse-pollerの修正による影響を受けない

影響なし

  • src/lib/assistant-response-saver.ts - cleanClaudeResponse/cleanGeminiResponseをimportしているが、これらはexport関数であり今回の修正対象(内部関数extractResponse())とは無関係。シグネチャ・動作に変更なし
  • src/lib/session-cleanup.ts - stopPollingをimportしているが、API変更なし
  • src/lib/cli-tools/manager.ts - stopPollingをimportしているが、API変更なし
  • src/app/api/worktrees/[id]/respond/route.ts - startPollingをimportしているが、API変更なし
  • src/app/api/worktrees/[id]/send/route.ts - startPollingをimportしているが、API変更なし
  • src/app/api/worktrees/[id]/start-polling/route.ts - startPollingをimportしているが、API変更なし
  • src/lib/prompt-detector.ts - 内部ロジック・インターフェースに変更なし(呼び出し元が渡す入力が変わるのみ)
  • src/lib/cli-patterns.ts - API/動作に変更なし
  • src/lib/status-detector.ts - response-pollerをimportしていない

テスト影響

テストファイル 影響 備考
tests/unit/lib/response-poller.test.ts 変更不要(既存テスト) / 新規テスト追加推奨 既存テストはcleanClaudeResponse()rawContent fallbackのみ。extractResponse()のプロンプト検出パスに対するテスト追加を推奨(テスト戦略セクション参照)
tests/unit/prompt-detector.test.ts 変更不要 prompt-detector自体に変更なし
src/lib/__tests__/assistant-response-saver.test.ts 変更不要 import先のexport関数に変更なし
tests/unit/session-cleanup.test.ts 変更不要 response-pollerstopPollingのみをモックしており、今回の修正対象(内部関数extractResponse())とは無関係
tests/unit/cli-tools/manager-stop-pollers.test.ts 変更不要 response-pollerstopPollingのみをモックしており、今回の修正対象(内部関数extractResponse())とは無関係

DB保存データへの影響

修正後、新しく保存されるpromptメッセージのcontent長がバッファ全体からlastCapturedLine以降に縮小する。既にDBに保存されている既存のpromptメッセージ(バッファ全体を含むもの)との表示上の一貫性は変わるが、これは期待された動作改善(前の会話の混入を防止)であり、DBマイグレーションや移行処理は不要である。

ラベル

  • bug

レビュー履歴

イテレーション 1 (2026-02-20) - Stage 1 レビュー

  • SF-1: 箇所2のstripAnsi未適用について明記し、修正時の判断観点を追加
  • SF-2: checkForResponse内promptDetection再検出への影響考慮を修正方針に追加
  • SF-3: startIndex決定ロジックの具体的な分岐(4パターン)と方針A/Bの選択肢を明記

イテレーション 1 (2026-02-20) - Stage 3 影響範囲レビュー

  • MF-1: auto-yes-manager.tsのpollAutoYes()への間接影響を影響範囲セクションに追記(DB保存contentの変化によるlastAnsweredPromptKey重複判定への影響)
  • SF-1: extractResponse()のテスト戦略(ヘルパー関数抽出/結合テスト/テスト用export)を修正方針に追加
  • SF-2: checkForResponse内rawContent影響分析を追記(rawContentがlastCapturedLine以降に縮小する点、Issue #235設計意図との整合性確認要)
  • SF-3: assistant-response-saver.tsの影響なし(cleanClaudeResponse/cleanGeminiResponseは変更対象外)を影響範囲に明記
  • NTH-1: テスト影響テーブルを影響範囲セクションに追加
  • NTH-2: DB保存済みメッセージへの影響(移行処理不要)を影響範囲セクションに追記

イテレーション 2 (2026-02-20) - Stage 5 レビュー

  • SF-1: auto-yes-manager.tsへの間接影響の説明を正確化。lastAnsweredPromptKeyはgeneratePromptKey()によるtype:question形式のキーであり、DB保存contentとは独立しているため影響なしと明記

イテレーション 2 (2026-02-20) - Stage 7 影響範囲レビュー

  • NTH-1: テスト影響テーブルにtests/unit/session-cleanup.test.tstests/unit/cli-tools/manager-stop-pollers.test.tsを「変更不要」として追加(stopPollingのみをモックしており修正対象外)

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