Repository navigation
fix(auto-yes): 途中送信の記憶の worktree ID 移送をこの PR から外す (#3706) - #3712
Merged
Merged
Conversation
## 決めたこと - 世代の token は「state そのものの同一性」で持つ。AutoYesPollerState の型は src/lib/polling/auto-yes-poller-state.ts(scope 外)にあり field を足せないため、 startAutoYesPolling が作る poller state オブジェクトを poller の token (PollerOwner と同じ考え方。restart・disable→enable で別オブジェクト、stop で削除)、 auto-yes-state の AutoYesState オブジェクトを許可の token(enable・disable のたびに 新しいオブジェクトが入る。期限切れは getAutoYesState が disabled を入れ直すので 同一性で外れる)とした。auto-yes-state.ts は変更不要だった。 - CLI セッションの epoch は既存の getEnterFallbackSessionEpoch(beginAgentSession が 進める)をそのまま使った(新設なし)。 - tick 冒頭で AutoYesTickGeneration を取り、capture の後・所有権確認の後・Enter fallback の fresh capture の後・送信関数の中の各待ちの後・detect の後・ isSessionMissing の後に isTickCurrent で照合。scheduleNextPoll(generation) は キーで引き直さず、自分の poller state が現役のときだけそこに timer を張る。 timer の closure も自分の poller state を渡し、置き換わった poller の timer は何もしない。 - 世代が外れた tick は何も変えずに終わる。ただし自分の poller が現役(許可の更新・ セッションの作り直しだけ)なら再予約して鎖を絶やさない。 - 送信関数: SendPromptAnswerParams.stillCurrent と sendSpecialKeys の options.shouldContinue(どちらも省略可能)。ガードの画面読み・キーの間・Enter 前の 待ち・多肢選択の再読の後に照合し、偽なら PromptAnswerAbortedError / SpecialKeysAbortedError で止める。省略時は sendSpecialKeys を従来の 2 引数で呼ぶので、 手動の送信・他の呼び手は不変。detectAndRespondToPrompt の generation も省略可能で、 省略時(直接の呼び手・既存テスト)は照合しない。 - 送信済みと中断の区別: 途中で止めた送信は回答としても error としても数えない (poller:auto-yes-answer-abandoned を keysSent つきで info)。送り終えた回答は task event と監査行を残し、error count・最終応答時刻は自分の世代の poller state に だけ書く。応答 poller の再開と画面配信は、セッションの epoch が変わっていれば しない(disable だけなら送った回答の返事を拾うために従来どおり再開する)。 - 許可の更新(enable 中の再 enable)で旧 tick の送信も止まる。1 tick 分の回答が 次の tick に遅れるだけで、鎖は続く。 ## 確かめたこと - tests/unit/polling/auto-yes-poller-generation-3706.test.ts(17 件。capture・所有権・ sender のガード読み・Enter 前の待ち・fallback の fresh capture を deferred / fake timer で止める): 陽性 12 件は直す前の実装(HEAD の 3 ファイル)で 12 件とも落ち、 陰性 5 件は直す前・後とも通ることを確認。 - tests/unit/lib/tmux-special-keys-abort-3706.test.ts(実 tmux.ts、execFile のみ stub)4 件。 - 関係するテスト 252 ファイル(auto-yes-poller / auto-yes-manager / prompt-answer-sender / tmux / auto-yes-state を参照するもの。auto-yes-poller-split-3411・ auto-yes-enter-fallback-3397 を含む)+ tests/unit/guards + tests/unit/docs: 296 files / 4900 tests 通過。既存の it/describe の名前・期待値は変えていない。 - npm run lint・npx tsc --noEmit 通過。node scripts/changelog-fragments.mjs check exit 0。 ## 対になる場所を探した結果 - 応答 poller(polling/response-poller*, poller-owner.ts)は既に PollerOwner で照合済み。 - 手動の回答(/prompt-response route)は stillCurrent を渡さないので不変(意図どおり)。 - hooks の permission-adjudication.ts は resolvePermissionRequest(同期)から answerPendingDecisionWithReceipt の呼び出しまでに await が無く、同じ形の欠陥は無かった。 - docs(module-reference / implementation-history ja・en)に pollAutoYes の待ちの 振る舞いを固定する記述は無かった。古い動きを固定する既存テストも無かった。 ## 本文に無い指摘 - src/lib/polling/auto-yes-poller-state.ts:16 AutoYesPollerState に明示の世代 field が 無い(今回はオブジェクトの同一性で代用)。将来 poller state を作り直さずに再利用する 変更が入ると token が壊れるので、型に generation を足すなら scope を広げて行う。 - src/lib/auto-yes-state.ts:200 getAutoYesState は読むだけで期限切れを disable に 書き換える(副作用つきの読み)。今回の照合も期限切れ時にこれを踏む(自分の世代の 許可に対してのみ)。
再指示 1(Codex 整合性レビューの P2 × 3)。7357e10 の上に足す。
## 決めたこと
### P2-1 送信中に CLI セッションが作り直された回答
- 区別は呼び手(finishAnsweredPrompt)で行い、監査の札の選び方には時刻の条件だけを
足した。chat-db は session epoch を知らないので epoch で選ばせるのは層が合わない。
代わりに recordAnsweredPrompt に省略可能な pendingSavedAtOrBefore(epoch ms)を足し、
Auto-Yes はセッションが変わったときだけ tick 開始時刻(generation.startedAt)を渡す。
キーが届いたのは旧プロセスで、新プロセスの prompt から保存された pending 札は
必ず tick 開始より後なので採られない。旧セッションの札(tick 前に保存)は従来どおり
回答済みになり、無ければ回答済みの行を新しく作る(監査は残る)。
残る端: 旧セッションの札が tick の途中に保存された場合は採られず、新しい行が
作られてその札は pending のまま残る(旧セッションの札なので新セッションには無害)。
- セッションが変わったときは dedup キー(lastAnsweredPromptKey/At)・最終応答時刻・
error count のリセット・task の prompt_answered_auto を書かない(今の task を引き直すと
新セッションの task に通知しうるため)。応答 poller の再開・画面配信は前回どおりしない。
### P2-2 Enter fallback の記録
- 記録に epoch を持たせた。recordEnterFallbackSent は sent.epoch(省略時は現在の epoch)が
現在の epoch と違えば書かず false を返す。getLastEnterFallback も別 epoch の記録を
返さない。poller は Enter の帰りで、自分の poller が現役かつ同じセッションのときだけ
enterFallbackSentKey と記録を書く(停止=forgetEnterFallback の後に復活させない)。
監査行(finishAnsweredPrompt)はどちらでも残す。
### P2-3 キーを送ってから止めた回答
- 中断(PromptAnswerAbortedError で keysSent=true)のとき、composite key ごとに
{promptKey, session epoch} を覚える(auto-yes-poller-state.ts、globalThis)。
poller state に持たせないのは、典型の停止が disable→enable で poller が作り直されるため。
次以降の tick は同じ epoch・同じ promptKey の prompt に答えず no_answer
(poller:auto-yes-skipped-partial-answer を 1 画面 1 回 WARN)。
- 半端な入力(`y` だけ、カーソル移動だけ)は画面から読み分けられないので、Auto-Yes は
消しも補完もせず人に任せる。prompt が画面から消える・別の prompt に変わる・
セッションが作り直される(epoch が変わる)と忘れる。stopAllAutoYesPolling と
stopAutoYesPollingByWorktree でも消す。キーを 1 つも送っていない中断は覚えない。
- AutoYesPollerState に明示の世代 field は足さなかった(同一性の照合で足りている)。
## 確かめたこと
- auto-yes-poller-generation-3706.test.ts に 10 件追加(計 27 件)。P2-1 陽性 1・陰性 1
(陽性は次の tick で新セッションの同じ質問に答えることまで確認)、P2-2 陽性 2
(relaunch・stop)・陰性 1、P2-3 陽性 2(renewal・disable→enable、次の 3 tick まで)・
陰性 3(prompt が消えて戻る・relaunch・キー送信前の中断)。陽性 5 件は直す前
(7357e10 の 4 ファイル)で落ち、陰性は前後とも通ることを確認。
- 関係するテスト 393 ファイル(auto-yes-*、prompt-answer-sender、tmux、chat-db、
current-output、enter-fallback ほか)+ tests/unit/guards + tests/unit/docs:
429 files / 7357 tests 通過。既存の it/describe の名前・期待値は変えていない。
- npm run lint・npx tsc --noEmit 通過。changelog-fragments check exit 0。
## 対になる場所を探した結果
- recordAnsweredPrompt の他の呼び手(/prompt-response route)は新引数を渡さず不変。
- recordEnterFallbackSent の本番の呼び手は poller だけ。getLastEnterFallback の読み手
(current-output、端末配信)は別 epoch の記録を受け取らなくなる(beginEnterFallbackSession
が既に削除しているので通常は差が出ない)。
- 手動回答の経路には途中中断が無い(stillCurrent を渡さない)ので P2-3 の対は無い。
- docs に該当の振る舞いを固定する記述・古い動きを固定するテストは無かった。
## 本文に無い指摘
- src/lib/polling/auto-yes-suppression-reason.ts(scope 外): 途中中断で人に任せた
prompt を lastSuppression として公開する理由が無いため、`cmate wait` はこの prompt を
Auto-Yes 待ちとして保留し続けうる。理由語の追加は別 Issue で。
- src/lib/prompt-answer-sender.ts: 世代と無関係な transport 失敗(`y` の後の Enter の
send-keys が失敗)でも半端な入力が残るが、error として数えるだけで P2-3 の記憶には
入れていない(今回は中断のみ)。
再指示 2(Codex 再レビューの P2 × 4)。7357e10・d58d691 の上に足す。 ## 決めたこと ### P2-1 後段の await 後の再開 - finishAnsweredPrompt は応答 poller と画面配信を始める直前(動的 import の await の後)に isSameSession(generation) を読み直す。前に取った sameSession は使い回さない。照合は session epoch(自分の世代の epoch)で、poller の置き換えは見ない — disable だけなら 送った回答の返事を拾うために再開する、という d58d691 の判断を保つ。 ### P2-2 途中送信の記憶のキー - promptKey(type+question)をやめ、partialAnswerScreenKey(type・question・選択肢の 番号と label・approvalTarget)にした。promptFrameKey が区別するもののうち、カーソル (option の isDefault)だけを除く。実測で、generic parser の approvalTarget は選択肢の パネルを `❯` ごと写すため、approvalTarget からも行頭のカーソル記号と字下げを除いた (除かないと、カーソルだけ動いた同じ画面が別 prompt に読まれ、陰性テストが落ちた)。 promptFrameKey 自体は変えていない。期限は付けない(prompt が消える・別 prompt・ session の作り直しで忘れる、は従来どおり)。 ### P2-3 worktree ID の移行 - auto-yes-poller-state.ts に migratePartialAnswerWorktreeIds(renames) を置き、 reconcile の migrateRuntimeState が Auto-Yes state の移送直後(poller 再開の前)に呼ぶ。 記憶の Map が poller-state にあるので移送関数も同じ場所に置いた(auto-yes-state.ts から 呼ぶと import が循環する)。移送は state と同じ 2 パス(ID の swap に耐える)。 移送先に既に記憶があれば、移ってきた記憶で置き換える:rename が成功したなら新しい 名前の session は無かったので、移送先の記憶は既に居ないプロセスについてのもの。 epoch は移送先キーの今の値にする(epoch の counter はキーごとで、rename されたプロセスが 新キーの今の session だから)。 ### P2-4 人に任せた prompt の公開 - suppressAndWarnOnce で lastSuppression に理由語 'partial-answer' を記録する(毎 tick 更新、WARN は 1 画面 1 回)。prompt Push の判定は policy-withheld になり通知され、 current-output の autoYes.lastSuppression にも載るので cmate wait は「回答中」として 待たない。 - 理由語は AutoYesSuppressionReason の union に入れず、auto-yes-suppression-reason.ts に PARTIAL_ANSWER_SUPPRESSION_REASON / AutoYesServerOnlySuppressionReason として別に置き、 記録の 1 か所で cast した。union に足すと、CLI の写し(api-responses.ts が同じ union を re-export)を通して src/cli/commands/wait.ts の Record<AutoYesSuppressionReason,string> (SUPPRESSION_CAUSE)と tests/unit/cli/config/cross-validation.test.ts の双方向の型照合が tsc で落ちる。どちらも scope 外。サーバ側の読み手(suppression-state・prompt-push-gate・ current-output-builder)は理由語を素通しするだけで、wait は知らない理由語を 「for a reason this CLI does not recognise (partial-answer)」とそのまま出す(文書化済みの 挙動)ので、実行時の扱いは正しい。 ## 確かめたこと - auto-yes-poller-generation-3706.test.ts に 8 件追加(計 35 件)。P2-1 陽性(audit の後の await 中に relaunch → 応答 poller を再開しない)・陰性、P2-2 陽性(gemini、同じ質問文の 別の選択肢には答える)・陰性(カーソルだけ動いた同じ一覧は保留のまま)、P2-3 陽性 (reconcileWorktreeSessions で移した後の新 poller が 3 tick 再入力しない)・陰性(記憶が 無ければ移した poller は従来どおり答える)、P2-4 陽性(lastSuppression=partial-answer、 decidePromptPush が policy-withheld)・陰性(通常の回答は suppression 無し・ auto-yes-answering)。陽性 4 件は直す前(d58d691)で落ち、陰性は前後とも通る。 - 関係するテスト 427 ファイル(auto-yes-*、prompt-answer-sender、tmux、chat-db、 current-output、worktree-session-reconcile、prompt-push-gate、suppression、 cross-validation、cli wait ほか)+ tests/unit/guards + tests/unit/docs: 462 files / 8019 tests 通過。既存の it/describe の名前・期待値は変えていない。 - npm run lint・npx tsc --noEmit 通過。changelog-fragments check exit 0。 ## 対になる場所を探した結果 - 応答 poller の再開を行うのは finishAnsweredPrompt だけ(Enter fallback も同じ関数を通る)。 - 途中送信の記憶を読む・書く場所は detectAndRespondToPrompt だけ。worktree 削除 (stopAutoYesPollingByWorktree)・全停止は d58d691 で消している。 - 理由語を列挙するもの: src/cli/commands/wait.ts:289 の SUPPRESSION_CAUSE、 src/cli/types/api-responses.ts:199 の re-export、tests/unit/cli/config/cross-validation.test.ts:91 の列挙。いずれも scope 外のため未変更(下の指摘)。docs に理由語の一覧は docs/module-reference.md(scope 外、断片で追記を依頼)。 ## 本文に無い指摘 - src/cli/commands/wait.ts:283 SUPPRESSION_CAUSE に 'partial-answer' の文言(例: "because an earlier answer was stopped part-way and its keys are still in the input")を足し、 src/lib/polling/auto-yes-suppression-reason.ts の union へ合流させ、 tests/unit/cli/config/cross-validation.test.ts:91 の列挙にも足す(3 つ同時でないと tsc が落ちる)。 合流後は auto-yes-poller.ts の cast を外せる。 - src/lib/session/worktree-session-reconcile.ts:613 移行は Enter fallback の記録と session epoch(auto-yes-enter-fallback-state.ts の Map、キーは worktree ID 込み)を移さない。 移行後は旧キーに残り、新キーの epoch は 0 から始まる。
再指示 3(Codex 3 回目の再レビュー、review-3706c.md の P2 × 3)。3 件とも b6bfac6 で足した migratePartialAnswerWorktreeIds とその reconcile からの呼び出しの中の指摘で、利用者の判断で この処理をこの PR から外し、別 Issue で設計し直す。 ## 決めたこと - auto-yes-poller-state.ts から migratePartialAnswerWorktreeIds を取り除き、それだけが使って いた import(buildCompositeKey・extractCliToolId・extractInstanceId・extractWorktreeId・ getEnterFallbackSessionEpoch)も d58d691 の時点の形へ戻した。 - src/lib/session/worktree-session-reconcile.ts は develop と同じ内容に戻した(差分 0)。 - auto-yes-poller-generation-3706.test.ts から、この PR で足した P2-3 移送のテスト(describe 「re-review P2-3: a worktree ID move carries the stopped answer」の陽性・陰性 2 件)と、 そのためだけに足した import(reconcileWorktreeSessions)・mock の項目 (migrateResponsePollerWorktreeIds・migrateWorktreeRooms)を取り除いた。 - changelog.d/3706.md から移送の一文を除いた。 - P2-1 の再照合、P2-2 の画面識別のキー、P2-4 の partial-answer の公開、7357e10・d58d691 の 変更は変えていない。 ## 確かめたこと - 生成テスト 33 件・tmux の中断テストを含む関係するテスト(tests/unit/polling、 tests/unit/lib/auto-yes*、worktree-session-reconcile.test.ts)+ tests/unit/guards + tests/unit/docs: 89 files / 1476 tests 通過。既存の it/describe の名前・期待値は変えていない (取り除いたのはこの PR で足したテストだけ)。 - npm run lint・npx tsc --noEmit 通過。changelog-fragments check exit 0。 - git grep で migratePartialAnswer の参照が src・tests に残っていないこと。 ## 本文に無い指摘 - 既知の残り(前回の P2-3): worktree ID の移行(reconcileWorktreeSessions)の後、途中送信の 記憶は旧キーに残り、新キーで再開した poller はそれを見ないため、キーを途中まで送って止めた 同じ prompt に再入力しうる(例: `y` の後に Enter を保留したまま移行 → 新 poller が `y` を もう一度打ち `yy` になる)。別 Issue で移送の設計(Enter fallback の記録・session epoch の 移送と合わせて)をやり直す。 - 前回までの指摘は変わらず残る: wait.ts の SUPPRESSION_CAUSE・union・cross-validation への 'partial-answer' の同時追加、reconcile が Enter fallback の記録と session epoch を移さない こと、transport 失敗による半端な入力は記憶の対象外、AutoYesPollerState に明示の世代 field が 無いこと、getAutoYesState の副作用つきの読み。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 15, 2026
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.
再指示 3(Codex 3 回目の再レビュー、review-3706c.md の P2 × 3)。3 件とも b6bfac6 で足した
migratePartialAnswerWorktreeIds とその reconcile からの呼び出しの中の指摘で、利用者の判断で
この処理をこの PR から外し、別 Issue で設計し直す。
決めたこと
いた import(buildCompositeKey・extractCliToolId・extractInstanceId・extractWorktreeId・
getEnterFallbackSessionEpoch)も d58d691 の時点の形へ戻した。
「re-review P2-3: a worktree ID move carries the stopped answer」の陽性・陰性 2 件)と、
そのためだけに足した import(reconcileWorktreeSessions)・mock の項目
(migrateResponsePollerWorktreeIds・migrateWorktreeRooms)を取り除いた。
変更は変えていない。
確かめたこと
tests/unit/lib/auto-yes*、worktree-session-reconcile.test.ts)+ tests/unit/guards +
tests/unit/docs: 89 files / 1476 tests 通過。既存の it/describe の名前・期待値は変えていない
(取り除いたのはこの PR で足したテストだけ)。
本文に無い指摘
記憶は旧キーに残り、新キーで再開した poller はそれを見ないため、キーを途中まで送って止めた
同じ prompt に再入力しうる(例:
yの後に Enter を保留したまま移行 → 新 poller がyをもう一度打ち
yyになる)。別 Issue で移送の設計(Enter fallback の記録・session epoch の移送と合わせて)をやり直す。
'partial-answer' の同時追加、reconcile が Enter fallback の記録と session epoch を移さない
こと、transport 失敗による半端な入力は記憶の対象外、AutoYesPollerState に明示の世代 field が
無いこと、getAutoYesState の副作用つきの読み。
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Refs #3706