Skip to content

Fix persistent mode to use CliUrl instead of spawning child process#118

Merged
PureWeen merged 1 commit intomainfrom
fix-dev-flow-loop
Feb 16, 2026
Merged

Fix persistent mode to use CliUrl instead of spawning child process#118
PureWeen merged 1 commit intomainfrom
fix-dev-flow-loop

Conversation

@PureWeen
Copy link
Owner

Problem

In Persistent mode, CreateClient() always set CliPath on the SDK options, causing the SDK to spawn a child copilot process via stdio — identical to Embedded mode. This defeated the purpose of Persistent mode (surviving app restarts via a headless server).

The root cause: CopilotClientOptions auto-discovers a bundled CliPath and sets UseStdio=true in its constructor. Setting CliUrl without first clearing these throws ArgumentException (they're mutually exclusive).

Fix

CreateClient() now properly branches:

  • Persistent mode: Clears CliPath=null, UseStdio=false, AutoStart=false, then sets CliUrl=http://{host}:{port} to connect to the existing headless server via TCP
  • Embedded mode: Resolves CLI path via bundled/system fallback chain

ServerManager.FindCopilotBinary() now falls back to the bundled binary when no global copilot install exists.

Compatibility with Redth's PRs

Reviewed PRs #58, #66, #78 — our changes are a deliberate improvement over PR #58's workaround (which made Persistent identical to Embedded due to CliUrl protocol issues). We fixed the root cause by properly clearing SDK constructor defaults. PRs #66 and #78 (direct sharing, auto-start) are unaffected.

Tests (47 new, 300 total)

File Tests Coverage
PersistentModeTests.cs 23 SDK mutual exclusivity, option building for all modes
CliPathResolutionTests.cs 16 Bundled binary existence, SDK defaults, fallback chain
InitializationModeTests.cs 10 Mode branching, CliUrl format, fallback scenarios
WsBridgeServerAuthTests.cs 8 Password/token auth contract, backward compat
ConnectionSettingsTests.cs +6 ServerPassword, DirectSharingEnabled, CliSource fields

All 300 tests pass with PolyPilot running (no port conflicts).

- CopilotService.CreateClient() now branches on Persistent vs Embedded mode:
  - Persistent: clears CliPath/UseStdio, sets CliUrl for TCP connection to headless server
  - Embedded: resolves CLI path via bundled/system fallback chain
- ServerManager.FindCopilotBinary() falls back to bundled binary when no global install
- Removed temporary ResumeTrace debug logging

Tests (47 new, 300 total):
- PersistentModeTests: SDK mutual exclusivity, option building for all modes
- CliPathResolutionTests: bundled binary existence, SDK defaults, fallback chain
- InitializationModeTests: mode branching, CliUrl format, fallback scenarios
- WsBridgeServerAuthTests: password/token auth contract, backward compat
- ConnectionSettingsTests: ServerPassword, DirectSharingEnabled, CliSource fields
@PureWeen PureWeen merged commit a7209bf into main Feb 16, 2026
PureWeen added a commit that referenced this pull request Feb 16, 2026
* CopilotService: catch StartAsync failures and set NeedsConfiguration; return early to avoid later 'Service not initialized'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix: dispose failed client and null reference after StartAsync failure

When StartAsync fails (e.g., persistent server down + no system copilot binary),
the catch blocks now dispose the failed CopilotClient and set _client = null.
This prevents a stale broken client reference that would cause NullReferenceException
on subsequent operations, and ensures the 'Service not initialized' guard checks
work correctly. Also fixes comment indentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add test proving StartAsync throws on unreachable server

Validates the premise of the InitializeAsync/ReconnectAsync try/catch fix:
CopilotClient.StartAsync() throws when connecting to a non-existent server,
confirming the error handling path is defending against a real failure mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Extract interfaces for CopilotService dependencies and add integration tests

- Extract IChatDatabase, IServerManager, IWsBridgeClient, IDemoService interfaces
- Concrete classes implement their respective interfaces
- CopilotService constructor accepts interfaces (internal ctor for test injection)
- DI registration forwards interfaces to concrete singletons
- Add TestStubs.cs with stub implementations for all interfaces
- Add CopilotServiceInitializationTests.cs with 15 integration tests covering:
  - Pre-initialization guard checks (CreateSession/ResumeSession throw)
  - Demo mode initialization and session creation
  - Embedded mode failure → NeedsConfiguration
  - Persistent mode failure → NeedsConfiguration
  - Mode transitions (Demo → Embedded)
  - OnStateChanged event firing
  - Session cleanup on reconnect

All 316 tests pass. MAUI build succeeds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add missing debug log in ReconnectAsync, suppress CS0067 warnings in stubs

- Add Debug log message in ReconnectAsync catch block to match InitializeAsync pattern
- Add #pragma warning disable/restore CS0067 around stub classes with unused events

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove stray TestSdk project and null.dll

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix session loss during mode switches (Embedded↔Persistent)

Root cause: SaveActiveSessionsToDisk() overwrites active-sessions.json with
only the currently in-memory sessions. During mode switches, sessions that
fail to restore (or haven't been restored yet) were permanently lost from
the file. If the app was killed mid-restore, the same data loss occurred.

Fix: SaveActiveSessionsToDisk now merges — it reads the existing file and
preserves entries whose session directory still exists on disk, even if
they aren't currently loaded in memory. This makes the operation safe to
call at any point during startup, restore, or mode switching.

Additional changes:
- Skip SaveActiveSessionsToDisk during restore (IsRestoring guard) to
  avoid unnecessary disk I/O per session
- Skip SessionStartEvent save during restore for same reason
- Track explicitly closed session IDs so merge doesn't re-add them
- Add LoadOrganization + ReconcileOrganization in ReconnectAsync (was
  missing vs InitializeAsync)
- Add detailed debug logging in RestorePreviousSessionsAsync
- Add 7 mode-switch integration tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix mode persistence, add fallback notice, and bug report button

- SetMode() in Settings.razor now calls settings.Save() immediately so the
  user's mode choice persists even if the app is killed before clicking
  Save & Reconnect (fixes mode reverting to Embedded on restart)
- Add FallbackNotice property to CopilotService — when persistent server
  can't start and we fall back to Embedded, a dismissable warning banner
  is shown on the Dashboard with a link to Settings
- Add 'Report Bug' button in the sidebar footer that expands inline to
  collect a description and auto-attach debug info (mode, session count,
  platform, crash log tail), then submits via gh CLI to PureWeen/PolyPilot
- Remove duplicate UiState/ActiveSessionEntry classes from test project
  (now compiled in via Compile Include from CopilotService.cs)
- Add tests for FallbackNotice, ClearFallbackNotice, mode persistence

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add CLI source/mode persistence tests, scenarios, and update repo docs

- Add 7 new unit tests: CliSource default, switching, independence from
  mode, serialization round-trip, mode switch preserving CliSource,
  ResolveBundledCliPath safety, and unique scenario IDs validation
- Add 3 new UI scenarios: CLI source switch persistence, mode persists
  without Save & Reconnect, bug report button visibility
- Add scenario cross-reference tests for new scenarios
- Update copilot-instructions.md:
  - Fix default mode (Persistent, not Embedded) for desktop
  - Document mode switching & merge-based session persistence
  - Document test safety rules (never call Save/Load, use Demo mode)
  - Add new test files to coverage section

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split sidebar footer into Report Bug and Fix/Feature buttons

Report Bug: creates a GitHub issue via gh CLI with debug info attached.
Fix/Feature: creates a worktree on a new branch, writes a prompt file
with the bug description + debug info + coding conventions, then
launches copilot CLI in a new Terminal.app window pointed at the
worktree. The prompt instructs copilot to fix the issue, add tests
and scenarios, then create a PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add settings resilience instruction to copilot prompt

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix terminal launch: write scripts to temp files to avoid shell quoting issues

The osascript -e '...' approach with UseShellExecute mangled the quotes.
Now writes both the shell script and AppleScript to temp files, which
osascript reads directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix terminal launch: use 'open -a Terminal' to avoid sandbox inheritance

osascript-spawned Terminal inherits the MAUI app's sandbox restrictions,
causing 'Permission denied' errors for the copilot CLI. Using 'open -a Terminal'
launches Terminal.app as a fully independent process with no sandbox.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add --yolo flag to copilot launch for non-interactive permission grants

Without --yolo, copilot CLI tries to prompt for directory/tool permissions
and fails with 'Permission denied and could not request permission from user'
when launched non-interactively in a new worktree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add inline 'Reconnect Now' hint when transport mode changes

Shows a yellow hint bar below the mode cards when the user switches
between Embedded/Persistent, with a Reconnect Now button. Hint
disappears after successful reconnect.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Make fix-it copilot launch interactive with --resume

Adds --resume flag so copilot stays interactive after processing the
initial prompt. User can follow up, provide more context, or guide
the fix directly in the terminal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use -i (interactive) flag instead of -p for copilot launch

-p is non-interactive and exits after completion. -i starts interactive
mode, executes the prompt, then keeps the session open for follow-up
conversation in the terminal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen deleted the fix-dev-flow-loop branch February 22, 2026 00:16
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