Chat UI stops updating mid-turn and thinking indicator stays stuck
## Summary
エージェントのターン中に socket.io イベントの配信が途切れ、「考え中…」インジケーターが消えずにスタックする。セッションは裏で正常に完了しているが、ページをリロードしないと結果が表示されない。
<img width="1876" height="666" alt="Image" src="https://github.com/user-attachments/assets/d1518045-dd51-4a18-ba08-dfb49179aa30" />
環境
- M4 Mac BookAir
- Safari
## 症状
エージェントのターン中に、ある時点から後続のイベント(tool_call、text、session_finished すべて)がブラウザに届かなくなる。
- セッションは裏で正常に動作・完了している
- 「考え中…」インジケーターが表示されたまま消えない
- ページをリロードすると jsonl から読み直されて正しく表示される
## 再現状況(7/1 確認)
`/mulmo-beta-feedback` スキルで Slack 読み取り → コレクション更新中に発生。
1. アシスタントの text イベント(「了解です。コレクションのレコードに issue 案のフィールドを書き込むだけですね。」)までは正常に表示
2. 直後の ToolSearch → manageCollection → 完了テキスト が一切表示されない
3. セッションは完了済み(jsonl に完了報告まで記録されている)
4. リロードで全内容が表示された
## 原因分析
2つの問題が重なっている。
### 1. socket.io 接続の瞬断でイベントが丸ごと消失する
ターン途中で socket.io 接続が瞬断すると、その間のイベントがすべて失われる。socket.io は再接続時にチャンネルを再登録するが、見逃したイベントの再送は行わない。`session_finished` も含めて消失するため、セッション完了を検知できない。
関連コード: `src/composables/usePubSub.ts` — `resendSubscriptions` on reconnect, but no mechanism to catch up on missed events.
### 2. クライアントに「考え中」を直接解除する手段がない
`handleSessionFinished()` は `session.isRunning = false` を直接セットしない(PR #192 で削除)。代わりに `sessions` チャンネルの pub/sub 通知 → `refreshSessionStates()` → サーバー API から差分取得、という間接経路に依存しているが、この通知自体も瞬断で消失する。
さらに `refreshSessionStates` はカーソルベースの差分フェッチ(`changeMs > cursor`)を使い、`changeMs` はディスク上のファイル mtime で判定される。`endRun()` 内の `persistHasUnread` が await されず fire-and-forget のため、ディスク書き込み完了前にクライアントの fetch が到着すると、mtime 未変更 → カーソルフィルタで除外 → 古い `isRunning: true` がそのまま使われる。
関連コード:
- `server/events/session-store/index.ts` L178-191 — `endRun()`: `persistHasUnread` が await されていない
- `src/composables/useSessionSync.ts` L33-53 — `refreshSessionStates()`: diff に含まれないセッションは古い値を使い続ける
- `src/composables/useSessionHistory.ts` L38-56 — `fetchSessions()`: カーソルベースの差分フェッチ
## 修正案
| 対策 | 内容 | 効果 |
|---|---|---|
| **A. `handleSessionFinished` で即座に `isRunning = false`** | PR #192 で削除されたコードの復元 | `session_finished` が届けば確実に解除。瞬断で `session_finished` 自体を見逃した場合は効果なし |
| **B. socket.io 再接続時にトランスクリプトを再取得** | `reconnect` イベントで `refreshSessionTranscript` + `refreshSessionStates` を呼ぶ | 瞬断中に見逃したイベントをリロードなしで回復 |
| **C. `endRun` で `persistHasUnread` を await してから通知** | ディスク書き込み完了後に `notifySessionsChanged` | カーソル差分の race condition を解消 |
A + B の組み合わせが最も確実。C は差分フェッチの信頼性向上として補助的に有効。
3 条评论