refactor(ipc): declare the SignalR wire DTOs once in ClaudeDo.Data

WorkerHub and IWorkerClient each carried their own copy of every record and
had already drifted — MergePreviewDto lost its verify fields on the client
side, two records disagreed on their name. They now live in Data/Wire.cs and
reach both sides via a <Using> item, so a hub signature change is a compile
error instead of a silently dropped JSON field.
This commit is contained in:
mika kuns
2026-08-26 13:55:40 +02:00
parent 39d8241360
commit 8930b564e4
13 changed files with 233 additions and 358 deletions
+4 -1
View File
@@ -177,7 +177,10 @@ Claude then calls `get_daily_prep_candidates`, picks an effort-aware subset capp
## SignalR Hub
`WorkerHub` is the canonical method list — grep it rather than trusting a doc inventory.
`WorkerHub` is the canonical method list — grep it rather than trusting a doc inventory. Its wire
DTOs live in `ClaudeDo.Data/Wire.cs` (shared with `IWorkerClient` via a `<Using>` item), **not** in
`WorkerHub.cs` — don't re-declare one locally, that's how `MergePreviewDto` lost its verify fields
on the client side.
Groups: execution · review/merge · conflict resolver · planning sessions · interactive ConPTY
launch specs · worktrees · agents/settings/lists · reports/notes/prep · diagnostics · usage.
`IWorkerClient` in `ClaudeDo.Ui` mirrors it.
@@ -27,6 +27,10 @@
<ApplicationIcon>ClaudeTaskWorker.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Using Include="ClaudeDo.Data.Wire" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
</ItemGroup>
+3 -171
View File
@@ -28,174 +28,6 @@ using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Worker.Hub;
public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt);
/// <summary>Git SHA the running worker was built from. Null when the build wasn't stamped
/// (e.g. a local dev build without the SourceRevisionId target).</summary>
public record WorkerBuildInfoDto(string? BuildSha);
public record AppSettingsDto(
string DefaultClaudeInstructions,
string DefaultModel,
int DefaultMaxTurns,
string DefaultPermissionMode,
int MaxParallelExecutions,
string WorktreeStrategy,
string? CentralWorktreeRoot,
bool WorktreeAutoCleanupEnabled,
int WorktreeAutoCleanupDays,
string? ReportExcludedPaths,
int StandupWeekday,
int DailyPrepMaxTasks,
List<string>? SessionSkills = null,
List<ModelPresetDto>? ModelPresets = null,
int UsageGateFiveHourPct = 80,
int UsageGateSevenDayPct = 90,
int MaxTurnsCeiling = 80,
// Throttle stages per bucket — dragged on the usage-monitor gauges, not typed in Settings.
int UsageThrottleFiveHourSoftPct = 50,
int UsageThrottleFiveHourHardPct = 65,
int UsageThrottleSevenDaySoftPct = 50,
int UsageThrottleSevenDayHardPct = 65,
bool AutoContinueOnUsageLimit = false);
// Per-model run defaults (effort + turn budget) edited in Settings -> General.
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
public record SessionSkillDto(
string Name,
string Description,
string SourceUrl,
string PinnedRef,
DateTimeOffset AddedAt);
public record WorktreeCleanupDto(int Removed);
public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
public record WorktreeOverviewDto(
string TaskId,
string TaskTitle,
ClaudeDo.Data.Models.TaskStatus TaskStatus,
string ListId,
string ListName,
string Path,
string BranchName,
string BaseCommit,
WorktreeState State,
string? DiffStat,
DateTime CreatedAt,
bool PathExistsOnDisk);
public record ForceRemoveResultDto(bool Removed, string? Reason);
public record PlanningMergeConflictStateDto(string PlanningTaskId, string SubtaskId);
public record PendingQuestionDto(string TaskId, string QuestionId, string Question);
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount);
public record SetTaskStatusResultDto(BaseDirtyWarningDto? BaseDirty);
// Verify* fields are always null on this path today -- the UI's live mergeability indicator never
// requests a verify run (that would mean an unrequested build on every preview poll); they exist so
// MergePreviewDto matches TaskMergeService.MergePreviewResult should a caller opt in later.
public record MergePreviewDto(
string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount,
int? VerifyExitCode = null, long? VerifyDurationMs = null, string? VerifyOutputTail = null);
public record MergeTargetsDto(
string DefaultBranch, IReadOnlyList<string> LocalBranches, string DefaultCommitMessage);
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDocumentDto> Files);
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false);
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
public record SeedResultDto(int Copied, int Skipped);
public record OnlineInboxStateDto(
bool Enabled,
string ApiBaseUrl,
string Authority,
string ClientId,
string Scopes,
string RedirectUri,
bool SignedIn,
int PollIntervalSeconds);
public record OnlineInboxConfigInput(
bool Enabled,
string ApiBaseUrl,
int PollIntervalSeconds,
string Authority,
string ClientId,
string Scopes,
string RedirectUri);
public record UsageLimitDto(
string Kind,
string Group,
double Percent,
string Severity,
DateTimeOffset? ResetsAt,
string? ScopeModelDisplayName,
bool IsActive);
public record UsageSnapshotDto(
double? FiveHourPercent,
DateTimeOffset? FiveHourResetsAt,
double? SevenDayPercent,
DateTimeOffset? SevenDayResetsAt,
IReadOnlyList<UsageLimitDto> Limits,
int FiveHourThresholdPct,
int SevenDayThresholdPct,
bool IsGateBlocked,
string? GateReason,
DateTime? FetchedAtUtc,
bool IsStale,
string? LastError,
int ConfiguredSlots,
int EffectiveSlots,
string? ThrottleBucket,
// Throttle stages per bucket, so the usage monitor can draw (and drag) them on each gauge.
int ThrottleFiveHourSoftPct,
int ThrottleFiveHourHardPct,
int ThrottleSevenDaySoftPct,
int ThrottleSevenDayHardPct);
public record ModelUsageRowDto(
DateOnly Date,
string Model,
string Scope,
long InputTokens,
long OutputTokens,
long CacheReadTokens,
long CacheCreationTokens,
int Messages,
double? CostUsd = null);
public record TaskUsageRowDto(
string TaskId,
string TaskTitle,
string ListId,
string ListName,
string? Model,
int Runs,
long TokensIn,
long TokensOut,
double? CostUsd = null,
int? Retries = null,
bool? Productive = null,
bool? OneShot = null);
public record TokenTrackerDashboardDto(bool Ok, string? Url, string? Error);
public record TokenTrackerStatusDto(
bool Installed,
string? Version,
bool NodeOk,
string? NodeVersion,
DateTime? LastFetchedUtc,
string? LastError,
int? FormatVersion,
int SessionCount);
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
{
private static readonly string Version =
@@ -240,7 +72,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
private readonly Runner.PendingQuestionRegistry _pendingQuestions;
private readonly LogRingBuffer? _logBuffer;
private readonly ISessionSkillRegistry _skillRegistry;
private readonly IInteractiveLaunchSpecService? _interactiveLaunchSpec;
private readonly InteractiveLaunchSpecService? _interactiveLaunchSpec;
private readonly WorktreeManager? _worktreeManager;
private readonly Data.Git.GitService? _git;
private readonly UsageSnapshotBuilder? _usageSnapshotBuilder;
@@ -275,7 +107,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
Runner.PendingQuestionRegistry pendingQuestions,
ISessionSkillRegistry skillRegistry,
LogRingBuffer? logBuffer = null,
IInteractiveLaunchSpecService? interactiveLaunchSpec = null,
InteractiveLaunchSpecService? interactiveLaunchSpec = null,
WorktreeManager? worktreeManager = null,
Data.Git.GitService? git = null,
UsageSnapshotBuilder? usageSnapshotBuilder = null,
@@ -1162,7 +994,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
_onlineInboxConfig.PollIntervalSeconds);
}
public void SetOnlineInboxConfig(OnlineInboxConfigInput input)
public void SetOnlineInboxConfig(OnlineInboxConfigInputDto input)
{
_onlineInboxConfig.Enabled = input.Enabled;
_onlineInboxConfig.ApiBaseUrl = input.ApiBaseUrl ?? "";