using System.Collections.ObjectModel; using System.Linq; using Avalonia.Threading; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; namespace ClaudeDo.Ui.Services; public sealed record WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc); sealed class IndefiniteRetryPolicy : IRetryPolicy { private static readonly TimeSpan[] _delays = [ TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30), ]; public TimeSpan? NextRetryDelay(RetryContext retryContext) => _delays[Math.Min(retryContext.PreviousRetryCount, _delays.Length - 1)]; } public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient { private readonly HubConnection _hub; private CancellationTokenSource? _startCts; private Task _retryLoopTask = Task.CompletedTask; private readonly object _startLock = new(); [ObservableProperty] private bool _isConnected; [ObservableProperty] private bool _isReconnecting; public ObservableCollection ActiveTasks { get; } = new(); public event Action? TaskStartedEvent; public event Action? TaskFinishedEvent; public event Action? TaskMessageEvent; public event Action? TaskUpdatedEvent; public event Action? TaskQuestionAskedEvent; public event Action? TaskQuestionResolvedEvent; public event Action, string>? HandoffRequestedEvent; public event Action? SessionHandoffRequestedEvent; public event Action>? ListHandlerStartRequestedEvent; public event Action? HandlerUsageWarningEvent; public event Action? HandlerHandoffDeferredEvent; public event Action? ConnectionRestoredEvent; public event Action? WorktreeUpdatedEvent; public event Action? ListUpdatedEvent; public event Action? WorkerLogReceivedEvent; public event Action? NotesUpdatedEvent; public event Action? PrepStartedEvent; public event Action? PrepLineEvent; public event Action? PrepFinishedEvent; public event Action? RefineStartedEvent; public event Action? RefineFinishedEvent; public event Action? UsageUpdatedEvent; public event Action? OperationProgressEvent; public event Action? OperationStartedEvent; public event Action? OperationEndedEvent; public event Action? PlanningMergeStartedEvent; public event Action? PlanningSubtaskMergedEvent; public event Action, bool>? PlanningMergeConflictEvent; public event Action? PlanningMergeAbortedEvent; public event Action? PlanningCompletedEvent; public event Action? PrimeFired; public string? LastApproveTarget { get; private set; } public IReadOnlyList GetActiveTasks() => ActiveTasks.ToList(); public WorkerClient(string signalRUrl) { _hub = new HubConnectionBuilder() .WithUrl(signalRUrl) .WithAutomaticReconnect(new IndefiniteRetryPolicy()) .AddJsonProtocol(options => { options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); }) .Build(); _hub.Reconnected += async _ => { Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; }); await SeedActiveTasksAsync(); Dispatcher.UIThread.Post(() => ConnectionRestoredEvent?.Invoke()); }; _hub.Reconnecting += _ => { Dispatcher.UIThread.Post(() => { IsConnected = false; IsReconnecting = true; }); return Task.CompletedTask; }; _hub.Closed += _ => { Dispatcher.UIThread.Post(() => { IsConnected = false; IsReconnecting = false; ActiveTasks.Clear(); }); return Task.CompletedTask; }; _hub.On("TaskStarted", (slot, taskId, startedAt) => { Dispatcher.UIThread.Post(() => { ActiveTasks.Add(new ActiveTaskDto(slot, taskId, startedAt)); TaskStartedEvent?.Invoke(slot, taskId, startedAt); }); }); _hub.On("TaskFinished", (slot, taskId, status, finishedAt) => { Dispatcher.UIThread.Post(() => { var existing = ActiveTasks.FirstOrDefault(t => t.TaskId == taskId); if (existing is not null) ActiveTasks.Remove(existing); TaskFinishedEvent?.Invoke(slot, taskId, status, finishedAt); }); }); _hub.On("TaskMessage", (taskId, line) => { Dispatcher.UIThread.Post(() => TaskMessageEvent?.Invoke(taskId, line)); }); _hub.On("TaskUpdated", taskId => { Dispatcher.UIThread.Post(() => TaskUpdatedEvent?.Invoke(taskId)); }); _hub.On("TaskQuestionAsked", (taskId, questionId, question) => { Dispatcher.UIThread.Post(() => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question)); }); _hub.On("TaskQuestionResolved", (taskId, questionId) => { Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId)); }); _hub.On, string>("HandoffRequested", (taskId, survivingTaskIds, nextPhase) => { Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase)); }); _hub.On("SessionHandoffRequested", (paneId, briefPath) => { Dispatcher.UIThread.Post(() => SessionHandoffRequestedEvent?.Invoke(paneId, briefPath)); }); _hub.On>("ListHandlerStartRequested", (listId, taskIds) => { Dispatcher.UIThread.Post(() => ListHandlerStartRequestedEvent?.Invoke(listId, taskIds)); }); _hub.On("HandlerUsageWarning", (taskId, message) => { Dispatcher.UIThread.Post(() => HandlerUsageWarningEvent?.Invoke(taskId, message)); }); _hub.On("HandlerHandoffDeferred", (taskId, dueAt) => { Dispatcher.UIThread.Post(() => HandlerHandoffDeferredEvent?.Invoke(taskId, dueAt)); }); _hub.On("WorktreeUpdated", taskId => { Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId)); }); _hub.On("ListUpdated", listId => { Dispatcher.UIThread.Post(() => ListUpdatedEvent?.Invoke(listId)); }); _hub.On("WorkerLog", (message, level, timestampUtc) => { Dispatcher.UIThread.Post(() => WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc))); }); _hub.On("OperationProgress", (opKey, phase, current, total) => { Dispatcher.UIThread.Post(() => OperationProgressEvent?.Invoke(opKey, phase, current, total)); }); _hub.On("OperationStarted", op => { Dispatcher.UIThread.Post(() => OperationStartedEvent?.Invoke(op)); }); _hub.On("OperationEnded", id => { Dispatcher.UIThread.Post(() => OperationEndedEvent?.Invoke(id)); }); _hub.On("PlanningMergeStarted", (planningTaskId, targetBranch) => { Dispatcher.UIThread.Post(() => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch)); }); _hub.On("PlanningSubtaskMerged", (planningTaskId, subtaskId) => { Dispatcher.UIThread.Post(() => PlanningSubtaskMergedEvent?.Invoke(planningTaskId, subtaskId)); }); _hub.On, bool>("PlanningMergeConflict", (planningTaskId, subtaskId, conflictedFiles, externallyDriven) => { Dispatcher.UIThread.Post(() => PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, conflictedFiles, externallyDriven)); }); _hub.On("PlanningMergeAborted", (planningTaskId, reason) => { Dispatcher.UIThread.Post(() => PlanningMergeAbortedEvent?.Invoke(planningTaskId, reason)); }); _hub.On("PlanningCompleted", planningTaskId => { Dispatcher.UIThread.Post(() => PlanningCompletedEvent?.Invoke(planningTaskId)); }); _hub.On("PrimeFired", (id, ok, msg, when) => { Dispatcher.UIThread.Post(() => PrimeFired?.Invoke(new PrimeFiredEvent(id, ok, msg, when))); }); _hub.On("NotesUpdated", () => Dispatcher.UIThread.Post(() => NotesUpdatedEvent?.Invoke())); _hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke())); _hub.On("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line))); _hub.On("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok))); _hub.On("RefineStarted", id => Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id))); _hub.On("RefineFinished", (id, ok, err) => Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err))); _hub.On("UsageUpdated", snapshot => Dispatcher.UIThread.Post(() => UsageUpdatedEvent?.Invoke(snapshot))); } public Task StartAsync() { lock (_startLock) { if (!_retryLoopTask.IsCompleted) return Task.CompletedTask; var old = _startCts; _startCts = new CancellationTokenSource(); old?.Cancel(); old?.Dispose(); _retryLoopTask = ConnectWithRetryAsync(_startCts.Token); } return Task.CompletedTask; } private async Task ConnectWithRetryAsync(CancellationToken ct) { var delays = new[] { 0, 2, 5, 10, 30 }; int attempt = 0; Dispatcher.UIThread.Post(() => IsReconnecting = true); while (!ct.IsCancellationRequested) { try { await _hub.StartAsync(ct); Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; }); await SeedActiveTasksAsync(); Dispatcher.UIThread.Post(() => ConnectionRestoredEvent?.Invoke()); return; } catch (OperationCanceledException) { return; } catch { var delay = delays[Math.Min(attempt++, delays.Length - 1)]; try { await Task.Delay(TimeSpan.FromSeconds(delay), ct); } catch (OperationCanceledException) { return; } } } } public async Task StopAsync() { _startCts?.Cancel(); try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ } try { await _hub.StopAsync(); } catch { /* swallow */ } } /// Invoke a hub method, returning default (null) when the worker is offline or errors. private async Task TryInvokeAsync(string method, params object?[] args) { var sw = System.Diagnostics.Stopwatch.StartNew(); var ok = false; try { var result = await _hub.InvokeCoreAsync(method, args); ok = true; return result; } catch (Exception ex) when (ex is not OperationCanceledException) { Serilog.Log.Warning(ex, "Hub call {method_name} failed", method); return default; } catch { return default; } finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); } } /// Times a void hub invoke without changing its exception/cancellation behavior. private async Task InvokeTimedAsync(string method, Func invoke) { var sw = System.Diagnostics.Stopwatch.StartNew(); var ok = false; try { await invoke(); ok = true; } catch (Exception ex) { Serilog.Log.Warning(ex, "Hub call {method_name} failed", method); throw; } finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); } } /// Times a hub invoke that returns a value, without changing its exception/cancellation behavior. private async Task InvokeTimedAsync(string method, Func> invoke) { var sw = System.Diagnostics.Stopwatch.StartNew(); var ok = false; try { var result = await invoke(); ok = true; return result; } catch (Exception ex) { Serilog.Log.Warning(ex, "Hub call {method_name} failed", method); throw; } finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); } } public async Task RunNowAsync(string taskId) { await InvokeTimedAsync("RunNow", () => _hub.InvokeAsync("RunNow", taskId)); } public async Task ContinueTaskAsync(string taskId, string followUpPrompt) { await InvokeTimedAsync("ContinueTask", () => _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt)); } public async Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer) { try { await InvokeTimedAsync("AnswerTaskQuestion", () => _hub.InvokeAsync("AnswerTaskQuestion", taskId, questionId, answer)); } catch { /* offline or already resolved — the UI clears optimistically */ } } public Task GetPendingQuestionAsync(string taskId) => TryInvokeAsync("GetPendingQuestion", taskId); public async Task ResetTaskAsync(string taskId) { await InvokeTimedAsync("ResetTask", () => _hub.InvokeAsync("ResetTask", taskId)); } public async Task MergeTaskAsync( string taskId, string targetBranch, bool removeWorktree, string commitMessage, bool autostash = false) { return await InvokeTimedAsync("MergeTask", () => _hub.InvokeAsync( "MergeTask", taskId, targetBranch, removeWorktree, commitMessage, autostash)); } public Task StartConflictMergeAsync(string taskId, string targetBranch, bool autostash = false) => InvokeTimedAsync("StartConflictMerge", () => _hub.InvokeAsync("StartConflictMerge", taskId, targetBranch, autostash)); public Task GetMergeConflictDocumentsAsync(string taskId) => InvokeTimedAsync("GetMergeConflictDocuments", () => _hub.InvokeAsync("GetMergeConflictDocuments", taskId)); public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => InvokeTimedAsync("WriteConflictResolution", () => _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent)); public Task ContinueConflictMergeAsync(string taskId) => InvokeTimedAsync("ContinueConflictMerge", () => _hub.InvokeAsync("ContinueConflictMerge", taskId)); public Task AbortConflictMergeAsync(string taskId) => InvokeTimedAsync("AbortConflictMerge", () => _hub.InvokeAsync("AbortConflictMerge", taskId)); public Task GetMergeTargetsAsync(string taskId) => TryInvokeAsync("GetMergeTargets", taskId); public async Task CancelTaskAsync(string taskId) { await InvokeTimedAsync("CancelTask", () => _hub.InvokeAsync("CancelTask", taskId)); } public async Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId) { try { await InvokeTimedAsync("DeleteTask", () => _hub.InvokeAsync("DeleteTask", taskId, CancellationToken.None)); return (true, null); } catch (HubException ex) { return (false, ex.Message); } catch (InvalidOperationException) { // Hub connection is not active (worker offline / not yet connected). return (false, "Worker is offline. Reconnect and try again."); } } public async Task WakeQueueAsync() { await InvokeTimedAsync("WakeQueue", () => _hub.InvokeAsync("WakeQueue")); } public async Task> GetAgentsAsync() => await TryInvokeAsync>("GetAgents") ?? []; public async Task RefreshAgentsAsync() { await InvokeTimedAsync("RefreshAgents", () => _hub.InvokeAsync("RefreshAgents")); } public async Task RestoreDefaultAgentsAsync() => await InvokeTimedAsync("RestoreDefaultAgents", () => _hub.InvokeAsync("RestoreDefaultAgents")); private async Task SeedActiveTasksAsync() { try { var active = await InvokeTimedAsync>("GetActive", () => _hub.InvokeAsync>("GetActive")); Dispatcher.UIThread.Post(() => { ActiveTasks.Clear(); foreach (var a in active) ActiveTasks.Add(new ActiveTaskDto(a.Slot, a.TaskId, a.StartedAt)); }); } catch (HubException) { // Expected: worker doesn't support GetActive yet } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"SeedActiveTasksAsync failed: {ex}"); } } public async ValueTask DisposeAsync() { _startCts?.Cancel(); try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ } await _hub.DisposeAsync(); } public Task GetAppSettingsAsync() => TryInvokeAsync("GetAppSettings"); public async Task UpdateAppSettingsAsync(AppSettingsDto dto) { await InvokeTimedAsync("UpdateAppSettings", () => _hub.InvokeAsync("UpdateAppSettings", dto)); } public async Task> GetPrimeSchedulesAsync() => await TryInvokeAsync>("ListPrimeSchedules") ?? new List(); public async Task UpsertPrimeScheduleAsync(PrimeScheduleDto dto) => await InvokeTimedAsync("UpsertPrimeSchedule", () => _hub.InvokeAsync("UpsertPrimeSchedule", dto)); public async Task DeletePrimeScheduleAsync(Guid id) { try { await InvokeTimedAsync("DeletePrimeSchedule", () => _hub.InvokeAsync("DeletePrimeSchedule", id)); } catch { /* offline */ } } private static string IsoDay(DateOnly d) => d.ToString("yyyy-MM-dd"); public Task GetWeekReportAsync(DateOnly start, DateOnly end) => TryInvokeAsync("GetWeekReport", IsoDay(start), IsoDay(end)); public Task GenerateWeekReportAsync(DateOnly start, DateOnly end) => InvokeTimedAsync("GenerateWeekReport", () => _hub.InvokeAsync("GenerateWeekReport", IsoDay(start), IsoDay(end))); public Task RunDailyPrepNowAsync() => InvokeTimedAsync("RunDailyPrepNow", () => _hub.InvokeAsync("RunDailyPrepNow")); public Task RunTicketImportNowAsync() => InvokeTimedAsync("RunTicketImportNow", () => _hub.InvokeAsync("RunTicketImportNow")); public Task RefineTaskAsync(string taskId) => InvokeTimedAsync("RefineTask", () => _hub.InvokeAsync("RefineTask", taskId)); public Task ClearMyDayAsync() => InvokeTimedAsync("ClearMyDay", () => _hub.InvokeAsync("ClearMyDay")); public async Task> GetDailyNotesAsync(DateOnly day) => await TryInvokeAsync>("GetDailyNotes", IsoDay(day)) ?? new List(); public async Task> GetDailyNotesBetweenAsync(DateOnly start, DateOnly end) => await TryInvokeAsync>("GetDailyNotesBetween", IsoDay(start), IsoDay(end)) ?? new List(); public async Task AddDailyNoteAsync(DateOnly day, string text) => await InvokeTimedAsync("AddDailyNote", () => _hub.InvokeAsync("AddDailyNote", IsoDay(day), text)); public async Task UpdateDailyNoteAsync(string id, string text) => await InvokeTimedAsync("UpdateDailyNote", () => _hub.InvokeAsync("UpdateDailyNote", id, text)); public async Task DeleteDailyNoteAsync(string id) => await InvokeTimedAsync("DeleteDailyNote", () => _hub.InvokeAsync("DeleteDailyNote", id)); public async Task GetLastPrepLogAsync() => await TryInvokeAsync("GetLastPrepLog") ?? string.Empty; public async Task> GetActiveOperationsAsync() => await TryInvokeAsync>("GetActiveOperations") ?? new List(); public async Task> GetRecentLogsAsync() => await TryInvokeAsync>("GetRecentLogs") ?? new List(); public async Task UpdateListAsync(UpdateListDto dto) { await InvokeTimedAsync("UpdateList", () => _hub.InvokeAsync("UpdateList", dto)); } public async Task UpdateListConfigAsync(UpdateListConfigDto dto) { await InvokeTimedAsync("UpdateListConfig", () => _hub.InvokeAsync("UpdateListConfig", dto)); } public Task GetListConfigAsync(string listId) => TryInvokeAsync("GetListConfig", listId); public async Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) { await InvokeTimedAsync("UpdateTaskAgentSettings", () => _hub.InvokeAsync("UpdateTaskAgentSettings", dto)); } public async Task> GetRepoImportFoldersAsync() => await InvokeTimedAsync>("GetRepoImportFolders", () => _hub.InvokeAsync>("GetRepoImportFolders")); public Task SetRepoImportFoldersAsync(List folders) => InvokeTimedAsync("SetRepoImportFolders", () => _hub.InvokeAsync("SetRepoImportFolders", folders)); public async Task> GetSshWorkspacesAsync() => await InvokeTimedAsync>("GetSshWorkspaces", () => _hub.InvokeAsync>("GetSshWorkspaces")); public Task SetSshWorkspacesAsync(List workspaces) => InvokeTimedAsync("SetSshWorkspaces", () => _hub.InvokeAsync("SetSshWorkspaces", workspaces)); public async Task> GetSessionSkillsAsync() => await TryInvokeAsync>("GetSessionSkills") ?? []; public Task> InstallSessionSkillAsync(string url) => InvokeTimedAsync>("InstallSessionSkill", () => _hub.InvokeAsync>("InstallSessionSkill", url)); public Task UpdateSessionSkillAsync(string sourceUrl) => InvokeTimedAsync("UpdateSessionSkill", () => _hub.InvokeAsync("UpdateSessionSkill", sourceUrl)); public Task RemoveSessionSkillAsync(string sourceUrl) => InvokeTimedAsync("RemoveSessionSkill", () => _hub.InvokeAsync("RemoveSessionSkill", sourceUrl)); public async Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status) { var result = await InvokeTimedAsync("SetTaskStatus", () => _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString())); return result?.BaseDirty; } public Task SetTaskDoneAsync(string taskId) => InvokeTimedAsync("SetTaskDone", () => _hub.InvokeAsync("SetTaskDone", taskId)); public Task UnsetTaskDoneAsync(string taskId) => InvokeTimedAsync("UnsetTaskDone", () => _hub.InvokeAsync("UnsetTaskDone", taskId)); public Task DequeueTaskAsync(string taskId) => InvokeTimedAsync("DequeueTask", () => _hub.InvokeAsync("DequeueTask", taskId)); public async Task ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) { LastApproveTarget = targetBranch; return await InvokeTimedAsync("ApproveReview", () => _hub.InvokeAsync("ApproveReview", taskId, targetBranch, autostash)); } public Task PreviewMergeAsync(string taskId, string targetBranch) => TryInvokeAsync("PreviewMerge", taskId, targetBranch); public async Task RejectReviewToQueueAsync(string taskId, string feedback) { await InvokeTimedAsync("RejectReviewToQueue", () => _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback)); } public async Task RejectReviewToIdleAsync(string taskId) { await InvokeTimedAsync("RejectReviewToIdle", () => _hub.InvokeAsync("RejectReviewToIdle", taskId)); } public async Task CancelReviewAsync(string taskId) { await InvokeTimedAsync("CancelReview", () => _hub.InvokeAsync("CancelReview", taskId)); } public async Task CleanupFinishedWorktreesAsync(string? listId = null) => await InvokeTimedAsync("CleanupFinishedWorktrees", () => _hub.InvokeAsync("CleanupFinishedWorktrees", listId)); public async Task ResetAllWorktreesAsync() => await InvokeTimedAsync("ResetAllWorktrees", () => _hub.InvokeAsync("ResetAllWorktrees")); public async Task> GetWorktreesOverviewAsync(string? listId) => await TryInvokeAsync>("GetWorktreesOverview", listId) ?? new List(); public async Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState) { try { var ok = await InvokeTimedAsync("SetWorktreeState", () => _hub.InvokeAsync("SetWorktreeState", taskId, newState)); return (ok, null); } catch (HubException ex) { return (false, ex.Message); } catch (Exception) { return (false, "Worker offline."); } } public async Task ForceRemoveWorktreeAsync(string taskId) => await InvokeTimedAsync("ForceRemoveWorktree", () => _hub.InvokeAsync("ForceRemoveWorktree", taskId)); public async Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("StartPlanningSessionAsync", () => _hub.InvokeAsync("StartPlanningSessionAsync", taskId, ct)); public async Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("ResumePlanningSessionAsync", () => _hub.InvokeAsync("ResumePlanningSessionAsync", taskId, ct)); public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("SubmitTaskForReview", () => _hub.InvokeAsync("SubmitTaskForReview", taskId, ct)); public async Task GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("GetInteractiveLaunchSpec", () => _hub.InvokeAsync("GetInteractiveLaunchSpec", taskId, ct)); public async Task GetAdHocLaunchSpecAsync(string directory, string paneId, CancellationToken ct = default) => await InvokeTimedAsync("GetAdHocLaunchSpec", () => _hub.InvokeAsync("GetAdHocLaunchSpec", directory, paneId, ct)); public async Task GetSshLaunchSpecAsync(string workspaceId, string paneId, CancellationToken ct = default) => await InvokeTimedAsync("GetSshLaunchSpec", () => _hub.InvokeAsync("GetSshLaunchSpec", workspaceId, paneId, ct)); public async Task GetSessionHandoffLaunchSpecAsync(string paneId, string cwd, string briefPath, string? taskId, CancellationToken ct = default) => await InvokeTimedAsync("GetSessionHandoffLaunchSpec", () => _hub.InvokeAsync("GetSessionHandoffLaunchSpec", paneId, cwd, briefPath, taskId, ct)); public async Task GetResumeSessionLaunchSpecAsync(string sessionId, string paneId, CancellationToken ct = default) => await InvokeTimedAsync("GetResumeSessionLaunchSpec", () => _hub.InvokeAsync("GetResumeSessionLaunchSpec", sessionId, paneId, ct)); public async Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default) => await InvokeTimedAsync("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync("GetMergeHelperLaunchSpec", taskIds, listId, handlerTaskId, model, ct)); public async Task CreateMergeHelperTaskAsync( IReadOnlyList taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default) => await InvokeTimedAsync("CreateMergeHelperTask", () => _hub.InvokeAsync("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct)); public async Task GetMergeHelperHandoffLaunchSpecAsync( string taskId, IReadOnlyList survivingTaskIds, string nextPhase, CancellationToken ct = default) => await InvokeTimedAsync("GetMergeHelperHandoffLaunchSpec", () => _hub.InvokeAsync("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, nextPhase, ct)); public Task ReportInteractiveSessionExitedAsync(string taskId, int exitCode, CancellationToken ct = default) => InvokeTimedAsync("ReportInteractiveSessionExited", () => _hub.InvokeAsync("ReportInteractiveSessionExited", taskId, exitCode, ct)); public async Task> GetHandlerOpenTasksAsync(string handlerTaskId, CancellationToken ct = default) => await InvokeTimedAsync>("GetHandlerOpenTasks", () => _hub.InvokeAsync>("GetHandlerOpenTasks", handlerTaskId, ct)); public async Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("GetPlanningStartLaunchSpec", () => _hub.InvokeAsync("GetPlanningStartLaunchSpec", taskId, ct)); public async Task GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("GetPlanningResumeLaunchSpec", () => _hub.InvokeAsync("GetPlanningResumeLaunchSpec", taskId, ct)); public async Task DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default) => await InvokeTimedAsync("DiscardPlanningSessionAsync", () => _hub.InvokeAsync("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct)); public async Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => await InvokeTimedAsync("FinalizePlanningSessionAsync", () => _hub.InvokeAsync("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct)); public async Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => await InvokeTimedAsync("GetPendingDraftCountAsync", () => _hub.InvokeAsync("GetPendingDraftCountAsync", taskId, ct)); public async Task> GetPlanningAggregateAsync(string planningTaskId) => await TryInvokeAsync>("GetPlanningAggregate", planningTaskId) ?? []; public async Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => await InvokeTimedAsync("BuildPlanningIntegrationBranch", () => _hub.InvokeAsync("BuildPlanningIntegrationBranch", planningTaskId, targetBranch)); public async Task ContinuePlanningMergeAsync(string planningTaskId) { await InvokeTimedAsync("ContinuePlanningMerge", () => _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId)); } public async Task AbortPlanningMergeAsync(string planningTaskId) { await InvokeTimedAsync("AbortPlanningMerge", () => _hub.InvokeAsync("AbortPlanningMerge", planningTaskId)); } public async Task> GetActiveExternalPlanningMergeConflictsAsync() => await TryInvokeAsync>("GetActiveExternalPlanningMergeConflicts") ?? []; public async Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) { await InvokeTimedAsync("QueuePlanningSubtasksAsync", () => _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct)); } public Task GetClaudeBinAsync() => TryInvokeAsync("GetClaudeBin"); public Task SetClaudeBinAsync(string? value) => TryInvokeAsync("SetClaudeBin", value); public Task GetOnlineInboxStateAsync() => TryInvokeAsync("GetOnlineInboxState"); public async Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => await InvokeTimedAsync("SetOnlineInboxConfig", () => _hub.InvokeAsync("SetOnlineInboxConfig", input)); public async Task SetOnlineInboxAuthAsync(string refreshToken) => await InvokeTimedAsync("SetOnlineInboxAuth", () => _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken)); public async Task ClearOnlineInboxAuthAsync() => await InvokeTimedAsync("ClearOnlineInboxAuth", () => _hub.InvokeAsync("ClearOnlineInboxAuth")); public Task GetUsageSnapshotAsync() => TryInvokeAsync("GetUsageSnapshot"); public Task RefreshUsageAsync() => TryInvokeAsync("RefreshUsage"); public async Task> GetModelUsageAsync(DateOnly from, DateOnly to) => await TryInvokeAsync>("GetModelUsage", from, to) ?? []; public async Task> GetTaskUsageAsync(DateOnly from, DateOnly to) => await TryInvokeAsync>("GetTaskUsage", from, to) ?? []; public Task GetTokenTrackerStatusAsync() => TryInvokeAsync("GetTokenTrackerStatus"); public Task RefreshTokenTrackerAsync() => TryInvokeAsync("RefreshTokenTracker"); public Task InstallTokenTrackerAsync() => TryInvokeAsync("InstallTokenTracker"); public Task OpenTokenTrackerDashboardAsync() => TryInvokeAsync("OpenTokenTrackerDashboard"); public Task GetTicketSettingsAsync() => TryInvokeAsync("GetTicketSettings"); public Task SetTicketFallbackListAsync(string? listId) => InvokeTimedAsync("SetTicketFallbackList", () => _hub.InvokeAsync("SetTicketFallbackList", listId)); public Task SetTicketApiBaseUrlAsync(string? baseUrl) => InvokeTimedAsync("SetTicketApiBaseUrl", () => _hub.InvokeAsync("SetTicketApiBaseUrl", baseUrl)); public Task SetTicketTokenAsync(string token) => InvokeTimedAsync("SetTicketToken", () => _hub.InvokeAsync("SetTicketToken", token)); public Task ClearTicketTokenAsync() => InvokeTimedAsync("ClearTicketToken", () => _hub.InvokeAsync("ClearTicketToken")); public Task TestTicketConnectionAsync() => TryInvokeAsync("TestTicketConnection"); public Task GetAzureDevOpsSettingsAsync() => TryInvokeAsync("GetAzureDevOpsSettings"); public Task SetAzureDevOpsTokenAsync(string token) => InvokeTimedAsync("SetAzureDevOpsToken", () => _hub.InvokeAsync("SetAzureDevOpsToken", token)); public Task ClearAzureDevOpsTokenAsync() => InvokeTimedAsync("ClearAzureDevOpsToken", () => _hub.InvokeAsync("ClearAzureDevOpsToken")); public Task TestAzureDevOpsConnectionAsync() => TryInvokeAsync("TestAzureDevOpsConnection"); public async Task> GetTicketProjectsAsync() => await TryInvokeAsync>("GetTicketProjects") ?? []; public Task ImportTicketsAsync(string listId) => InvokeTimedAsync("ImportTickets", () => _hub.InvokeAsync("ImportTickets", listId)); // IWorkerClient explicit implementations (drop typed return values) async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct) => await StartPlanningSessionAsync(taskId, ct); async Task IWorkerClient.ResumePlanningSessionAsync(string taskId, CancellationToken ct) => await ResumePlanningSessionAsync(taskId, ct); async Task IWorkerClient.DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren, CancellationToken ct) => await DiscardPlanningSessionAsync(taskId, dequeueQueuedChildren, ct); async Task IWorkerClient.FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks, CancellationToken ct) => await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct); async Task IWorkerClient.GetPendingDraftCountAsync(string taskId, CancellationToken ct) => await GetPendingDraftCountAsync(taskId, ct); }