Skip to content

fix: route orchestrator messages through dispatch pipeline even with image attachments#590

Merged
PureWeen merged 5 commits intomainfrom
fix/orchestrator-image-dispatch
Apr 16, 2026
Merged

fix: route orchestrator messages through dispatch pipeline even with image attachments#590
PureWeen merged 5 commits intomainfrom
fix/orchestrator-image-dispatch

Conversation

@PureWeen
Copy link
Copy Markdown
Owner

Problem

When a user sends a message to an orchestrator session that includes image attachments, the dispatch pipeline was completely bypassed. The message went directly to the orchestrator's SDK session via SendPromptAsync, and the orchestrator's response (containing @worker blocks) was never parsed or dispatched to workers.

Root Cause

Dashboard.razor line ~1922 had a guard that checked for empty imagePaths before routing through the dispatch pipeline. With images present, the code fell through to the direct send path, skipping SendToMultiAgentGroupAsync entirely.

Changes

Dashboard.razor

  • Orchestrator messages always route through dispatch regardless of image attachments
  • Images are stripped with a user-visible warning (SDK doesn't support image forwarding to workers)

CopilotService.Organization.cs

  • Added LogDispatchError method for persistent dispatch error logging
  • Fixed ResumeOrchestrationIfPendingAsync to reset stale ReflectionState after resume completion

Testing

All 3433 tests pass (12 pre-existing ExternalSessionScannerTests failures due to missing node).

Previously, messages with image attachments sent to orchestrator sessions
bypassed SendToMultiAgentGroupAsync entirely, going directly through
SendPromptAsync. The orchestrator would produce @worker blocks in its
response, but nobody parsed them — workers were never dispatched.

Changes:
- Dashboard.razor: Always route orchestrator messages through the dispatch
  pipeline. Images are stripped (pipeline doesn't support forwarding them)
  with a user-visible warning. This ensures @worker blocks are always
  parsed and workers are dispatched.
- Dashboard.razor: Dispatch errors now logged via LogDispatchError (persisted
  to event-diagnostics.log) instead of Console.WriteLine (invisible).
- CopilotService.Organization.cs: Added LogDispatchError method for
  persisting dispatch failures to diagnostics log.
- CopilotService.Organization.cs: ResumeOrchestrationIfPendingAsync now
  resets stale ReflectionState (IsActive=false, CompletedAt set) after
  completing resume synthesis. Without this, StartGroupReflection returns
  early on the next user prompt because the old IsActive=true persists
  across app restarts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen
Copy link
Copy Markdown
Owner Author

PureWeen commented Apr 16, 2026

🔍 Multi-Model Code Review — PR #590 (Final — Round 5)

fix: route orchestrator messages through dispatch pipeline even with image attachments


CI Status

⚠️ No CI checks reported on the fix/orchestrator-image-dispatch branch.


Complete Finding History (Rounds 1–5)

# Finding Introduced Resolved Final Status
1 ReflectionState mutated on background thread R1 R2 ✅ Fixed — InvokeOnUI
2 TOCTOU race on fresh reflection cycle R1 R4 ✅ Fixed — StartedAt guard + InvokeOnUI + UTC normalization
3 Warning text misleading R1 R2 ✅ Fixed — "Images dropped"
4 No test coverage R1 R3 ✅ Fixed — 3 targeted tests
5 group! in async closure R1 R2 ✅ Fixed — locals captured
6 LogDispatchError passthrough R1 R2 ✅ Improved
7 DateTime.Now vs UtcNow mismatch R3 R4 ✅ Fixed — ToLocalTime()
8 Early-exit paths miss ReflectionState reset R4 R5 ✅ Fixed — ClearPendingOrchestrationAndResetState helper at all 6 sites
9 Temp file leak for dropped images R4 R5 ✅ Fixed — File.Delete cleanup

Round 5 Assessment

What's New in v5

  1. Helper method extracted: ClearPendingOrchestrationAndResetState(PendingOrchestration pending) consolidates ClearPendingOrchestration() + ReflectionState reset + InvokeOnUI + OnOrchestratorPhaseChanged into a single method
  2. All 6 ClearPendingOrchestration() sites in the resume flow now use the helper (verified in diff: 6 call sites)
  3. Temp file cleanup added: foreach (var p in imagePaths) { try { File.Delete(p); } catch { } } when images are dropped for orchestrator dispatch

Verified Correct

  • ✅ Core fix: orchestrator messages with images route through dispatch pipeline
  • ✅ Users get clear warning when images are dropped
  • ✅ ReflectionState reset is thread-safe (InvokeOnUI), TOCTOU-protected (StartedAt guard), timezone-correct (ToLocalTime())
  • ✅ All resume-flow exit paths (group not found, orchestrator missing, exception, cancellation, no workers, happy path) properly reset stale ReflectionState
  • ✅ Temp image files cleaned up when dropped
  • ✅ Error handling improved (persistent diagnostics logging + user-visible error messages + UI refresh)
  • groupId/groupName captured as locals before async closures
  • ✅ 3 tests cover stale reset, inactive no-op, and fresh-cycle TOCTOU protection

No New Issues Found

All 9 findings across 5 rounds of review are resolved.


Recommendation

Approve

This PR has been through 5 rounds of iterative multi-model review, addressing 9 findings spanning threading safety, race conditions, DateTime correctness, test coverage, error handling, temp file management, and API consistency. The implementation is thorough and well-tested.

PureWeen and others added 4 commits April 16, 2026 10:26
- Wrap ReflectionState reset in InvokeOnUI to match UI-thread-only
  convention (fixes threading bug and TOCTOU race)
- Update image-strip warning to accurately communicate total image loss
- Capture group name as local before async closure (null safety)
- Add 2 behavioral tests for ReflectionState reset after resume

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The InvokeOnUI callback could reset a fresh ReflectionState created by a
user prompt between ClearPendingOrchestration() and the callback firing.
Now captures PendingOrchestration.StartedAt and compares it to the cycle's
StartedAt — a fresh cycle (newer timestamp) is left untouched.

Added test: ReflectionState_FreshCycleNotResetByStaleResume

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PendingOrchestration.StartedAt uses DateTime.UtcNow while
ReflectionCycle.StartedAt uses DateTime.Now. In UTC-negative timezones
(all Americas), the raw comparison incorrectly kills fresh cycles.
Apply the existing ToLocalTime() pattern from line 3091-3093.

Updated test to use DateTime.UtcNow for staleStartedAt to match
the production clock source.

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

Extract ClearPendingOrchestrationAndResetState() helper that combines
ClearPendingOrchestration + ReflectionState reset + phase notification.
Applied to all 6 ClearPendingOrchestration sites in the resume flow:
- Group not found
- Orchestrator session not found
- Exception handler
- Cancellation requested
- No worker responses
- Happy path (was inline, now uses helper)

Also: delete temp image files when images are dropped for orchestrator
dispatch, instead of leaking them until OS temp cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen merged commit 82a84fe into main Apr 16, 2026
@PureWeen PureWeen deleted the fix/orchestrator-image-dispatch branch April 16, 2026 18:24
PureWeen added a commit that referenced this pull request Apr 17, 2026
…#527)

## Problem

When a user adds an existing repo via **Existing Folder** (e.g. their
`dotnet/maui` clone), PolyPilot:
1. Created a **bare clone** of the entire repo at `~/.polypilot/repos/`
(redundant - user already has it)
2. Created **nested worktree checkouts** inside the user's repo at
`.polypilot/worktrees/{branch}/`
3. The user's **existing checkout was never used** for sessions

For huge repos like MAUI (~5GB), this wasted significant disk space and
time.

Additionally, when both a URL-based repo and a local folder pointed to
the same remote, both incorrectly used the same `BareClonePath`, causing
the local folder to be ignored.

## Changes

### Skip bare clone for Existing Folder repos
- `AddRepositoryFromLocalAsync` now points `BareClonePath` directly at
the user's existing repo instead of creating a redundant bare clone
- `EnsureRepoCloneInCurrentRootAsync` detects non-bare repos (`.git`
dir/file) and skips clone management

### Separate repos for URL vs local folder
- When a URL-based repo already exists, adding the same repo from a
local folder creates a **separate** `RepositoryInfo` with a distinct ID
(`{baseId}-local-{pathHash}`)
- The original URL-based repo's `BareClonePath` is never overwritten
- Idempotent: adding the same local folder twice returns the same repo

### Validate bare clone before reuse
- Corrupt or partial bare clone directories are detected and removed
before re-cloning

### Reuse existing checkout for same-branch sessions
- `CreateWorktreeAsync` checks for an existing registered worktree on
the requested branch before creating a new one
- Only matches centralized worktrees (under `~/.polypilot/worktrees/`),
never external user checkouts

### Remove nested worktree strategy
- All worktrees now go to the centralized `~/.polypilot/worktrees/`
directory
- Removed `localPath` parameter from `CreateWorktreeAsync`,
`CreateSessionWithWorktreeAsync`, and all UI callers

### Bug fixes
- Fixed `Path.Combine` producing backslashes for Unix-style path in
`BuildContinuationTranscript`
- Fixed `FindActiveLockPid` process name filter to include `testhost`
- Fixed repo picker to show full repo name instead of last dash-segment
(#570)
- Fixed existing repo and group name migration to URL-derived format
(#570)

### Orchestrator image dispatch fix (cherry-picked from PR #590)
- Orchestrator messages now always route through the dispatch pipeline
even with image attachments
- Previously, image attachments caused the dispatch pipeline to be
bypassed entirely

## Testing
All 3433 tests pass (12 pre-existing ExternalSessionScannerTests
failures due to missing `node`).

Test coverage includes:
- 11 `AddExistingRepoTests` (local folder behavior, URL vs local
separation, idempotency, validation errors, ID format, reconciliation)
- 43 `RepoManagerTests` (URL/ID parsing, clone management, worktree
reuse, git exclude entries)
- 34 `WorktreeStrategyTests` (strategy behaviors, worktree creation)

---------

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