Skip to content

Fix session freezing, add Copilot Console, harden thread safety#153

Merged
PureWeen merged 4 commits intomainfrom
fix/session-freezing
Feb 20, 2026
Merged

Fix session freezing, add Copilot Console, harden thread safety#153
PureWeen merged 4 commits intomainfrom
fix/session-freezing

Conversation

@PureWeen
Copy link
Owner

@PureWeen PureWeen commented Feb 20, 2026

Summary

Fixes sessions stuck showing "Thinking" forever, adds a "Copilot Console" context menu action, and hardens thread safety in the Dashboard render pipeline.

Changes

Bug Fix: Render Throttle Race (stuck "Thinking")

  • HandleComplete was using ScheduleRender() (debounced timer) which could silently drop the final render after session completion
  • Root cause: streaming events consumed the 500ms throttle window, so CompleteResponse's OnStateChanged was dropped
  • Fix: HandleComplete now calls InvokeAsync(StateHasChanged) directly, bypassing both throttle and debounce
  • All state mutations wrapped inside InvokeAsync for thread safety
  • Added _disposed guards to prevent StateHasChanged on disposed component

New Feature: Copilot Console

  • Adds ">_ Copilot Console" menu item to session context menus
  • Launches copilot --resume <sessionId> --yolo in Terminal.app
  • Uses PlatformHelper.ShellEscape() (single-quote escaping) to prevent shell injection

Security: Shell Injection Prevention

  • Extracted ShellEscape() to PlatformHelper for testability and reuse
  • All dynamic values are single-quote escaped before embedding in bash scripts

Tests

  • 5 new ShellEscape tests covering plain text, single quotes, special chars, empty strings
  • 1 new CompletionRace test documenting the throttle race condition
  • All 690 tests pass

Review

4-model fleet review (Opus 4.6, GPT-5.3 Codex, Sonnet 4.6, Gemini 3 Pro):

  • 3/4 consensus: Thread-safety race on completedSessions.Remove in ContinueWith callback → fixed
  • 1/4 (GPT-5.3): Disposal race on delayed continuation → fixed with _disposed guard
  • 4/4: Shell injection fix is correct

PureWeen and others added 4 commits February 20, 2026 10:01
Dashboard.HandleComplete used ScheduleRender() which goes through a
50ms debounced timer pipeline. When CompleteResponse fires OnStateChanged
followed by OnSessionComplete, the OnStateChanged is throttled (500ms
window from recent streaming events), and the ScheduleRender timer can
fail to produce a render — leaving cards permanently stuck on 'processing'.

Fix: Call StateHasChanged() directly in HandleComplete. This is safe
because we're already on the UI thread (called synchronously from
CompleteResponse → OnSessionComplete). The Blazor render happens after
the current callback completes, by which time IsProcessing is already
false.

Root cause confirmed via MauiDevFlow CDP inspection: navigating away
from Dashboard and back caused a fresh render that correctly showed
all sessions as idle, proving IsProcessing was false in the C# model
but the Blazor DOM was stale from a missed render.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new menu item that launches copilot --resume <sessionId> --yolo
in Terminal.app, allowing the user to interact with the same session
from the CLI. Uses the same shell script + 'open -a Terminal' pattern
as the existing Fix with Copilot feature for proper sandbox escape.

The existing Terminal action (opens directory) is preserved below it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract ShellEscape to PlatformHelper for testability and reuse
- Use single-quote escaping to prevent shell injection in Copilot Console
- Use InvokeAsync(StateHasChanged) in HandleComplete for defensive thread safety
- Add 5 ShellEscape tests covering special chars, empty strings, quotes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap all state mutations inside InvokeAsync to prevent thread-pool
  threads from racing with UI-thread rendering on non-thread-safe
  collections (HashSet, Dictionary)
- Add _disposed guard to both InvokeAsync lambdas to prevent
  StateHasChanged on a disposed component after navigation
- Found by 4-model fleet review (Opus, GPT-5.3, Sonnet consensus)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen changed the title Fix stuck Thinking: render immediately on session completion Fix session freezing, add Copilot Console, harden thread safety Feb 20, 2026
@PureWeen PureWeen merged commit dbe318b into main Feb 20, 2026
6 checks passed
PureWeen added a commit that referenced this pull request Feb 21, 2026
…edge

Add comprehensive documentation of the recurring stuck-session bug pattern
(7 PRs, 16 fix/regression cycles) to copilot-instructions.md:

- Full cleanup checklist for all IsProcessing=false paths
- Table of all 7 paths with locations
- 7 common mistakes with PR references where each occurred
- Staleness check and IsResumed clearing documentation
- Cross-thread volatile field requirements
- ProcessingGeneration guard explanation
- Watchdog diagnostic log tag additions

This knowledge was hard-won across PRs #141, #147, #148, #153, #158,
#163, #164 and should prevent future regressions by making the invariants
explicit and discoverable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen deleted the fix/session-freezing branch February 22, 2026 00:15
PureWeen added a commit that referenced this pull request Feb 25, 2026
## Problem
After app restart, resumed sessions that were mid-turn show
**Thinking...** with a Stop button. The user must manually click Stop
every time. The existing watchdog waited 600s (10 min!) before clearing
stuck IsProcessing.

## Solution
Add a **30s resume quiescence timeout** for sessions that receive zero
SDK events after restart. If no events flow within 30s of app start, the
session is cleared as stuck.

### Key design decisions (informed by 4-model consultation: Opus 4.6,
Sonnet 4.6, Codex 5.3, GPT-5.1):

1. **30s quiescence** — short enough users don't wait, long enough for
SDK reconnect (~5s typical, 6x safety margin)
2. **Event-gated** — only fires when \HasReceivedEventsSinceResume ==
false\. Once events start flowing, transitions to normal 120s/600s
timeout tiers
3. **Seed from DateTime.UtcNow, NOT file time** — all 3 models
independently flagged that seeding from events.jsonl would cause
immediate kills for sessions >15s old (exact PR #148 regression pattern)
4. **Reuses existing watchdog fire path** — no new IsProcessing cleanup
code, all 8 invariants preserved

### Timeout tiers (3-tier, was 2-tier):
| Condition | Timeout |
|-----------|---------|
| Resumed, zero events since restart | **30s** (NEW) |
| Normal processing, no tools | 120s |
| Active tools / resumed with events / multi-agent | 600s |

## Tests
- **16 new regression guard tests** covering quiescence edge cases, seed
time safety, exhaustive timeout matrix
- Updated existing tests to use \ComputeEffectiveTimeout\ helper
mirroring production 3-tier formula
- **108 total watchdog+recovery tests pass** ✅

## Regression history context
This code has been through 7 PRs of fix/regression cycles (PRs
#141#147#148#153#158#163#164). The most relevant precedent: PR
#148 added a 10s resume timeout that killed active sessions. Our 30s
timeout avoids this by being event-gated and seeded from UtcNow.

Fixes the 'click Stop on every restart' UX issue.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant