This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Build the solution
dotnet build
# Run the application
dotnet run --project BuildMonitor/BuildMonitor.csproj
# Clean build artifacts
dotnet cleanBuildMonitor is a desktop ImGui-based application that monitors CI/CD build statuses across multiple providers (GitHub Actions and Azure DevOps). The architecture follows a hierarchical data model with concurrent updating, priority-based scheduling, and adaptive rate limiting.
The application maintains a nested hierarchy of build data:
- BuildProvider (e.g., GitHub, AzureDevOps) → Owner → Repository → Build → Run
Each level is stored in ConcurrentDictionary collections to support thread-safe updates from async operations.
Owner Semantics by Provider:
- GitHub: Owners represent GitHub users or organizations
- Azure DevOps: Owners represent projects within an organization (the account ID is the organization name)
The codebase uses a "sync" pattern where domain entities (Build, Run) are paired with synchronization classes:
- Build / BuildSync: The Build entity stores data; BuildSync manages polling intervals, priority calculation, and async updates
- Run / RunSync: Same pattern for individual workflow runs
BuildSync and RunSync objects track when entities should be updated based on their state and calculate request priorities:
- High Priority (0): Running/in-progress builds (users care most about these)
- Medium Priority (1): Recent failures or builds with recent activity (within last hour)
- Low Priority (2): Completed/successful builds, discovery operations
Priority affects both update frequency and whether requests are made when API budget is constrained.
- Main Loop (BuildMonitor.cs): Calls
UpdateAsync()every frame - Provider Refresh (every 300s): Updates repositories and builds from providers
- Skipped when provider is in low budget mode (< 15% remaining API calls)
- Logs: "Skipping discovery due to low budget"
- Build Updates (30-120s, priority-based):
- High priority: 30s interval
- Medium priority: 60s interval
- Low priority: 120s interval
- Filtered by provider's
MaxAllowedPrioritybased on budget
- Run Updates (10-60s, adaptive): Updates only ongoing runs
- Dynamically adjusts interval based on estimated time remaining (ETA)
- Clamped between 10s minimum and 60s maximum
- Only high-priority requests (ongoing runs)
The update cycle creates BuildSync/RunSync entries in global collections (BuildSyncCollection, RunSyncCollection). Each sync object:
- Tracks elapsed time with a
Stopwatch - Calculates whether it should update via
ShouldUpdateproperty - Determines its priority via
Priorityproperty - Is pruned when orphaned (parent entity deleted) or completed (runs only)
BuildProvider is an abstract base class with JSON polymorphic serialization support. Currently supported providers:
- GitHub: Uses Octokit library for GitHub Actions API
- AzureDevOps: Uses Microsoft.TeamFoundation libraries for Azure DevOps REST API
New providers should:
- Inherit from BuildProvider
- Add
[JsonDerivedType]attribute to BuildProvider base class - Implement the four abstract update methods:
UpdateRepositoriesAsync(Owner): Discover/update repositories for an ownerUpdateBuildsAsync(Repository): Discover/update build definitions (workflows) for a repositoryUpdateBuildAsync(Build): Fetch recent runs for a specific buildUpdateRunAsync(Run): Update the status of a specific run
- Register in
BuildMonitor.OnStart()by adding toAppData.BuildProviders - Implement provider-specific menu actions via
ShowMenu()override
The application uses a GetOrAdd pattern to ensure existing entities are updated when new data arrives, preventing data loss and keeping properties in sync with the remote APIs.
Repository Updates:
- Uses
ConcurrentDictionary.GetOrAdd()instead ofTryAdd()to get existing or create new - Always updates mutable properties after getting the entity:
IsPrivate: Updated when repository visibility changesIsArchived: Updated when repository is archived/unarchivedIsFork: Updated when repository fork status changes
- Only triggers
QueueSaveAppData()when actual changes occur - Logs separately: new repositories, updated repositories, archived repositories removed
Build Updates:
- Uses
GetOrAdd()pattern for build/workflow definitions - Updates
BuildNameif the workflow file or build definition is renamed - Detects changes by comparing current value with API value
- Only saves when new builds are discovered or names change
Run Updates:
- Run entities are always updated in place (no GetOrAdd needed)
UpdateRunFromWorkflowAsync()(GitHub) andUpdateRunFromBuild()(Azure DevOps) always update properties- Updates: Status, Started, LastUpdated, Duration, Branch, Errors
- Errors are cleared when run status changes from failure to non-failure
- Triggers
Build.UpdateFromRun()to keep parent build state in sync
Benefits of Update Strategy:
- Repository visibility changes are reflected without manual intervention
- Renamed workflows/build definitions don't create duplicate entries
- No data accumulation from stale entities
- Efficient AppData saves (only when changes occur)
- Better logging distinguishes between discovery and updates
Each BuildProvider tracks its operational status with visual indicators in the status bar:
- ProviderStatus.OK (green): Operating normally
- ProviderStatus.RateLimited (yellow): API rate limit hit, includes backoff delay and reset time
- ProviderStatus.AuthFailed (red): Authentication failed, credentials cleared
- ProviderStatus.Error (magenta): Connection or other error
Status is displayed in a status bar at the top of the UI with:
- Color indicator showing status
- Provider name and status label
- Rate limit consumption display (e.g., "4532/5000")
- Tooltip with detailed status message and rate limit reset time
The application implements sophisticated rate limit management to avoid hitting API limits:
Budget Tracking:
- Tracks remaining API calls (
RateLimitBudgetRemaining) and total limit (RateLimitBudgetLimit) - Updates budget from successful API responses via
UpdateRateLimitBudget() - Calculates budget percentage:
BudgetPercentage(0.0 to 1.0)
Budget Thresholds:
- Low Budget Mode (< 15% remaining): Skips low-priority requests and discovery operations
- Critical Budget Mode (< 5% remaining): Only processes high-priority requests (running builds)
MaxAllowedPriorityreturns the maximum priority level allowed given current budget
Adaptive Pacing:
CalculateAdaptivePacing(): Spreads remaining requests evenly across the time window until reset- Reserves 10% of budget (or 50 requests minimum) for unexpected requests
- Calculates delay per request:
timeUntilReset / usableBudget * 1.1(with 10% safety margin) - Clamped between 100ms minimum and 30s maximum
Wait Time Calculation:
GetRateLimitWaitTime(): Returns the appropriate delay before the next request- When fully rate limited: waits until reset time (capped at 10 minutes)
- Otherwise: uses the larger of adaptive pacing delay or current
RateLimitSleep - Includes 5-second buffer for clock skew when waiting for reset
Rate Limit Recovery:
ClearStatus(): ResetsRateLimitSleepback to base delay (500ms) when recovering- Status automatically clears on first successful request after rate limiting
Provider-Level Authentication:
- Each provider has an
AccountIdandToken(stored in AppData, persisted) - Set via "Set Credentials" menu item (two-step popup: AccountId, then Token)
- Cleared automatically on
AuthorizationExceptionor 403 Forbidden responses
Owner-Level Authentication (GitHub only):
- Each owner can have an optional
Tokenproperty (overrides provider token) - Enables access to private repositories in different organizations
- Set via "Providers → GitHub → Set Owner Token" submenu
- Clear via "Providers → GitHub → Set Owner Token → Clear Owner Token" submenu
- Owner names in the menu show "(token)" indicator if owner has a token
HasValidCredentials(owner)checks owner token first, then falls back to provider token
GitHub Client Management:
- Maintains cache of
GitHubClientinstances per token (TokenClientsdictionary) - Uses
AsyncLocal<GitHubClient>for current client context (CurrentClientLocal) SetCurrentClient(owner)must be called before each API operation- Prevents race conditions when concurrent requests use different owner tokens
Azure DevOps Client Management:
- Creates
VssConnectionwithVssBasicCredentialusing AccountId and Token - Gets
ProjectHttpClientandBuildHttpClientfrom connection UpdateAzureDevOpsClientCredentials()recreates clients when credentials change- Organization URL format:
https://dev.azure.com/{AccountId}
- All data updates happen asynchronously via Task-based operations
ConcurrentDictionaryis used throughout for thread-safe collection accessBuildMonitor.SyncLockexists but is only used for menu renderingRequestSemaphorelimits concurrent requests per provider (default: 5)- Rate limiting uses adaptive delays calculated per request via
GetRateLimitWaitTime() - Active requests are tracked in
ActiveRequestsdictionary for UI feedback
Multi-Tab Layout:
The UI uses a tabbed interface (OwnerTabPanel) with dynamic tab management:
- "All" Tab: Shows builds from all owners across all providers
- Owner/Project Tabs:
- GitHub: One tab per owner (user or organization)
- Azure DevOps: One tab per account (the organization itself)
- Tab IDs format:
"GitHub:{ownerName}"or"ADO:{accountId}"
- "Logs" Tab: Application logs with color-coded levels and auto-scrolling
Tabs are dynamically created/removed as owners are discovered or removed. Selected tab is persisted in AppData.
Build Table Columns:
- Status (60px): Color indicator or radial progress bar
- Owner (150px): Owner/organization/project name
- Repository (150px): Repository name with property icons
- Build Name (200px): Workflow/build definition name (cleaned of
.ymland path prefixes) - Branch (150px): Git branch name
- Status (80px): Text status (Pending, Running, Success, Failure, Canceled)
- Last Run (180px): Timestamp of last run start (local time with timezone)
- Duration (80px): Duration of the run (format:
hh:mm:ssord.hh:mm:ss) - Estimate (80px): Estimated total duration for the build
- History (80px): Last 5 non-canceled runs as color indicators
- Progress (100px): Radial progress bar + percentage (ongoing builds only)
- ETA (80px): Estimated time remaining (ongoing builds only)
- Errors (200px): Error messages from failed runs (clickable for details)
- Next Update (100px): Countdown to next poll with radial progress indicator
Repository Icons: Repositories display nerd font icons to indicate properties:
\uf023(lock): Private repository\uf126(code fork): Forked repository\uf187(archive): Archived repository
Empty Repositories: Repositories without workflows/builds are shown with:
- Gray status indicator
- Owner and repository name (with property icons)
- "No workflows" text in gray in the Build Name column
- Hidden when any build name, branch, or status filter is active
Progress Indicators:
The UI uses radial progress bars (from ImGuiWidgets.RadialProgressBar) for:
- Build Status Column: Ongoing builds show progress instead of color indicator
- Progress Column: Radial bar + percentage text for ongoing builds
- Next Update Column: Countdown visualization showing time until next poll
Filtering:
- Filter row below table headers with search boxes for each column
- Supports multiple filter types (via
TextFilterType): Contains, Exact, Wildcard, Regex - Match options: Case-sensitive, whole word, etc.
- Filters are persisted per column in AppData
- Empty repositories hidden when build name, branch, or status filters are active
Column Width Management:
- Widths are persisted in
AppData.ColumnWidthsdictionary - Default widths defined in
DefaultColumnWidths - Manual pointer arithmetic used to read column widths from ImGui's native structs
- Workaround for Hexa.NET.ImGui struct layout bug (8-byte size difference)
SaveColumnWidth()saves when width changes by more than 1px
Right-clicking on any build row opens a context menu with the following actions:
Repository Actions:
- Open Repository in Browser
- Copy Repository URL
Workflow Actions:
- Open Workflow in Browser
- Copy Workflow URL
Branch Actions:
- Open Branch in Browser
- Copy Branch URL
Latest Run Actions:
- Open Latest Run in Browser
- Copy Latest Run URL
GitHub API Actions (GitHub provider only):
- Re-run Latest Workflow: Re-runs a completed workflow (disabled for running builds)
- Cancel Running Workflow: Cancels an in-progress workflow (only shown for running builds)
- Trigger Workflow on Branch: Dispatches a new workflow run on the selected branch
Data Refresh:
- Refresh Build Data: Forces immediate update by calling
BuildSync.ResetTimer()
All actions that modify build state (rerun, cancel, trigger) use ExecuteGitHubApiAction() which:
- Runs the action asynchronously in a background task
- Automatically triggers a build refresh on success
- Logs but does not crash on
ApiExceptionfailures
The Logs tab provides a real-time view of application logs:
Features:
- Color-coded log levels:
- Debug: Gray
- Info: White
- Warning: Yellow
- Error: Red
- Format:
[HH:mm:ss.fff] [LEVEL] message - Auto-scrolling: Stays at bottom when already scrolled to bottom
- Clear logs button at top
- Scrollable region with horizontal scrollbar support
Log Sources:
- Provider operations (authentication, rate limiting, errors)
- API requests (start/completion with duration)
- Build status transitions (started, succeeded, failed)
- Discovery operations (owners, repositories, builds)
- Budget management decisions
Failed runs display error information fetched from provider-specific job logs:
GitHub Error Fetching:
- Errors are fetched via
FetchRunErrorsAsync()when a run transitions to failure state or is a failure without errors - Job logs retrieved via
GitHubJobs.GetLogs() - Parsed with
ParseLogForErrors()using:- GitHub Actions error annotations:
##[error]message - Common error patterns: regex
(?:^|\s)error\s*:(case-insensitive)
- GitHub Actions error annotations:
- Errors are deduplicated using
HashSet<string>(case-insensitive) - Limited to first 10 errors to avoid UI overload
- Prefixed with job name:
[{job.Name}] {errorMessage} - Falls back to failed step names if no log errors found
- Further falls back to
[{job.Name}] Failedif no steps failed
UI Display:
- Errors shown in red text in the Errors column
- Text is ellipsized if longer than column width (with "..." suffix)
- Clickable to open a popup with full error details
- Popup shows build name, branch, and all errors with word wrapping
- Tooltip on hover shows full error text
- Errors cleared when run status changes from failure to non-failure
Build duration estimation uses sophisticated statistical methods via DurationEstimator:
Estimation Strategy:
- Sample Selection: Uses last 20 successful runs, ordered by start time (most recent first)
- Branch-Specific Estimation: When estimating for a branch:
- First attempts estimate using only runs from that branch
- Falls back to overall build estimation if branch has insufficient data (< 3 samples)
- Outlier Removal: Uses IQR (Interquartile Range) method with 1.5× multiplier (Tukey fence)
- Calculates Q1 (25th percentile) and Q3 (75th percentile)
- Removes values outside
[Q1 - 1.5*IQR, Q3 + 1.5*IQR] - Falls back to median if too many samples removed (< 3 remaining)
- Exponentially Weighted Average: Recent runs have higher weight
- Weight decreases by 70% for each older sample (decay factor: 0.3)
- Most recent run has weight 1.0, next has 0.7, next has 0.49, etc.
- Formula:
weight = (1 - decayFactor)^iwhere i is age index
Branch-Specific Estimation:
Build.CalculateEstimatedDuration(branch): Estimates for a specific branchBuild.CalculateEstimatedDuration(): Estimates across all branches- Run ETA calculation uses branch-specific estimation:
Run.CalculateETA()callsBuild.CalculateEstimatedDuration(Branch) - More accurate than global estimates for workflows that vary significantly by branch
Estimation Statistics:
DurationEstimator.GetEstimationStats(): Returns detailed statistics for debugging- Statistics include: sample count, min, max, median, final estimate, whether filtering was used
Usage:
- Used by UI to show estimated duration in "Estimate" column
- Used to calculate ETA (Estimated Time Remaining) for ongoing runs
- Drives adaptive update intervals in
RunSync.UpdateAsync()
File Menu:
- Clear Data: Clears all repositories and builds for all providers (preserves provider credentials)
- Exit: Closes the application
Providers Menu: Each provider has a submenu with provider-specific actions:
GitHub Provider Menu:
- Set Credentials: Two-step popup for AccountId and Token
- Discover All Owners: Automatically discovers user and all organizations
- Fetches authenticated user via
CurrentClient.User.Current() - Fetches all organizations via
CurrentClient.Organization.GetAllForCurrent() - Adds each as an owner (user and orgs)
- Fetches authenticated user via
- Add Owner: Manually add an owner by name
- Set Owner Token (submenu): Per-owner token management
- Lists all owners, shows "(token)" indicator for owners with tokens
- Opens popup to set token for selected owner
- Clear Owner Token (nested submenu): Remove token from selected owner
Azure DevOps Provider Menu:
- Set Credentials: Two-step popup for AccountId (organization name) and Token (PAT)
- Discover All Projects: Discovers all projects in the organization
- Fetches projects via
ProjectClient.GetProjects() - Creates an owner entry for each project
- Creates a repository entry (project acts as both owner and repository)
- Fetches projects via
- Add Owner: Manually add a project by name
AppData Structure:
Persisted via ktsu.AppDataStorage to JSON file:
WindowState: ImGui window position, size, and stateBuildProviders: Dictionary of provider instances (serialized polymorphically)ColumnWidths: Dictionary of column name to widthSelectedOwnerTabId: Currently selected tab ID- Filter settings per column (text, type, match options):
FilterOwner,FilterOwnerType,FilterOwnerMatchOptionsFilterRepository,FilterRepositoryType,FilterRepositoryMatchOptionsFilterBuildName,FilterBuildNameType,FilterBuildNameMatchOptionsFilterBranch,FilterBranchType,FilterBranchMatchOptionsFilterStatus,FilterStatusType,FilterStatusMatchOptions
Save Batching:
QueueSaveAppData(): Marks data for saving without immediate writeSaveSettingsIfRequired(): Called each frame, saves if queued- Batches multiple changes within a single frame into one write
Data Cleared on Auth Failure:
- Provider
AccountIdandTokencleared on authentication failure - Provider status set to
AuthFailed - Owner tokens preserved (only provider token cleared)
The codebase uses ktsu.Semantics.Strings for type-safe string identifiers:
BuildProviderName,BuildProviderAccountId,BuildProviderTokenOwnerName,OwnerIdRepositoryName,RepositoryIdBuildName,BuildIdRunName,RunIdBranchName
These prevent mixing up different types of identifiers and enable type-safe conversions via .As<T>().
Example:
OwnerName ownerName = "microsoft".As<OwnerName>();
BuildId buildId = workflowId.ToString().As<BuildId>();- BuildMonitor.cs: Main application class with UI rendering, update orchestration, and tab management
- AppData.cs: Persistent application state and settings
- BuildProvider.cs: Abstract base class with status tracking, rate limiting, and budget management
- Providers/GitHub.cs: GitHub Actions implementation with Octokit API integration
- Providers/AzureDevOps.cs: Azure DevOps implementation with Microsoft.TeamFoundation libraries
- Owner.cs: Represents a user/organization (GitHub) or project (Azure DevOps)
- Repository.cs: Represents a repository with properties (IsPrivate, IsArchived, IsFork)
- Build.cs: Represents a build definition/workflow with duration estimation
- Run.cs: Represents a workflow run with status and error information
- BuildSync.cs: Synchronization wrapper for Build with priority-based update scheduling
- RunSync.cs: Synchronization wrapper for Run with adaptive update intervals
- RequestPriority.cs: Priority enum and BuildSync class with priority logic
- DurationEstimator.cs: Statistical duration estimation with IQR outlier removal and exponential weighting
API Client:
- Uses Octokit library (
OctokitNuGet package) - Maintains per-token client cache to avoid race conditions
- Creates
GitHubClientwithProductHeaderValueandCredentials - Token format: Personal Access Token (PAT) with
repoandworkflowscopes
Repository Discovery:
- For authenticated user:
GitHubRepository.GetAllForCurrent()(includes private repos)- Filters to only repos owned by the user (not all accessible repos)
- For other users:
GitHubRepository.GetAllForUser(owner)(public repos only) - For organizations:
GitHubRepository.GetAllForOrg(owner)(respects org visibility) - Uses
GetOrAdd()to get existing or create new repository - Updates properties:
IsPrivate,IsArchived,IsForkon each refresh - Archived repositories are removed from tracking
- Logs:
"{newRepos} new, {updatedRepos} updated, {archivedRepos} archived removed"
Workflow/Build Discovery:
GitHubWorkflows.List(owner, repo): Gets all workflows in repository- Uses
GetOrAdd()to get existing or create new build - Updates
BuildNameif workflow file is renamed - Maps
Workflow.IdtoBuildIdandWorkflow.NametoBuildName
Run Fetching:
GitHubRuns.ListByWorkflow(): Gets last 10 runs for a workflow- Maps workflow run status/conclusion to
RunStatusenum - Logs status transitions (started, succeeded, failed)
Run Updates:
GitHubRuns.Get(): Fetches single run by ID- Updates on status change: Pending → Running → Success/Failure/Canceled
Error Fetching:
GitHubJobs.List(): Gets all jobs for a runGitHubJobs.GetLogs(): Gets log text for each failed job- Parses logs for
##[error]annotations and error patterns
Rate Limit Handling:
- Updates budget from
ApiInfo.RateLimitafter each successful request - Detects rate limits via 403 Forbidden +
X-RateLimit-Remaining: 0header - Parses reset time from
X-RateLimit-Resetheader (Unix timestamp) - Distinguishes 403 rate limit from 403 auth failure by checking headers
GitHub-Specific Actions:
RerunWorkflowAsync():GitHubRuns.Rerun()CancelWorkflowAsync():GitHubRuns.Cancel()TriggerWorkflowAsync():GitHubActions.Workflows.CreateDispatch()with branch reference
API Client:
- Uses Microsoft.TeamFoundation libraries (
Microsoft.TeamFoundationServer.Client, etc.) - Creates
VssConnectionwith organization URI:https://dev.azure.com/{AccountId} - Uses
VssBasicCredentialwith empty username and Personal Access Token (PAT) - Gets
ProjectHttpClientfor project/repository operations - Gets
BuildHttpClientfor build definition and run operations
Project/Owner Discovery:
ProjectClient.GetProjects(): Gets all projects in the organization- Each project becomes an owner (project name →
OwnerName) - Each project also creates a repository entry (project acts as both)
- Uses
GetOrAdd()to ensure repository entry exists for each project - Repository ID uses project GUID
Build Definition Discovery:
BuildClient.GetDefinitionsAsync(projectName): Gets all build definitions in project- Uses
GetOrAdd()to get existing or create new build - Updates
BuildNameif build definition is renamed - Maps
BuildDefinitionReference.IdtoBuildId - Maps
BuildDefinitionReference.NametoBuildName - Logs when new build definitions are discovered
Build/Run Fetching:
BuildClient.GetBuildsAsync(project, definitions, top): Gets last 10 builds for a definition- Maps
Build.IdtoRunIdandBuild.BuildNumbertoRunName - Branch name extracted from
SourceBranch(removesrefs/heads/prefix)
Run Updates:
BuildClient.GetBuildAsync(project, buildId): Fetches single build by ID- Uses
StartTimeor falls back toQueueTimefor run start - Uses
FinishTimeor current time for ongoing runs - Maps
BuildStatusandBuildResulttoRunStatus:NotStarted→ PendingInProgress/Cancelling→ RunningCompleted+Succeeded/PartiallySucceeded→ SuccessCompleted+Failed→ FailureCompleted+Canceled→ Canceled
Error Handling:
- Catches
VssServiceResponseExceptionfor HTTP errors - 401 Unauthorized →
OnAuthenticationFailure() - 429 Too Many Requests →
OnRateLimitExceeded() - Other
VssServiceException→ Sets Error status HttpRequestException→ Sets Error status
Limitations:
- No error log fetching implemented yet (Azure DevOps uses different log API)
- No workflow actions (rerun, cancel, trigger) - Azure DevOps API differs from GitHub
- Uses ktsu custom SDK (
ktsu.Sdk,ktsu.Sdk.App) in the .csproj - Targets .NET 10.0
- Requires
AllowUnsafeBlocksfor ImGui interop (column width pointer arithmetic) - Dependencies managed via Central Package Management (Directory.Packages.props)
Key Dependencies:
Hexa.NET.ImGui: Dear ImGui bindings for .NETktsu.ImGui.*: ktsu wrappers and widgets for ImGuiOctokit: GitHub API client libraryMicrosoft.TeamFoundationServer.Client: Azure DevOps API clientMicrosoft.VisualStudio.Services.Client: Azure DevOps service connectionktsu.Semantics.Strings: Type-safe string wrappersktsu.AppDataStorage: JSON persistence for application state
- Create a new class inheriting from
BuildProvider - Add
[JsonDerivedType(typeof(NewProvider), nameof(NewProvider))]toBuildProvider.cs - Implement required abstract methods:
UpdateRepositoriesAsync: Query provider API for repositoriesUpdateBuildsAsync: Query for build definitions/workflowsUpdateBuildAsync: Fetch recent runs for a buildUpdateRunAsync: Update a single run's status
- Override
ShowMenu()for provider-specific menu items - Implement authentication via inherited credential popup or custom logic
- Handle provider-specific rate limiting in request wrapper method
- Register provider in
BuildMonitor.OnStart():needsSave |= AppData.BuildProviders.TryAdd(NewProvider.BuildProviderName, new NewProvider());
IMPORTANT: Use GetOrAdd Pattern for Entity Updates
When implementing provider methods, always use GetOrAdd() instead of TryAdd() to ensure existing entities are updated:
// ❌ WRONG - Only adds new, doesn't update existing
Repository repository = owner.CreateRepository(name, id);
repository.IsPrivate = apiRepo.Private;
if (owner.Repositories.TryAdd(id, repository))
{
BuildMonitor.QueueSaveAppData();
}
// ✅ CORRECT - Updates existing or creates new
bool isNew = false;
Repository repository = owner.Repositories.GetOrAdd(id, _ =>
{
isNew = true;
return owner.CreateRepository(name, id);
});
// Always update mutable properties
bool hasChanges = false;
if (repository.IsPrivate != apiRepo.Private)
{
repository.IsPrivate = apiRepo.Private;
hasChanges = true;
}
// Only save when changes occur
if (isNew || hasChanges)
{
BuildMonitor.QueueSaveAppData();
}This pattern ensures:
- Existing entities receive property updates (e.g., repository visibility changes)
- No duplicate entities are created
- Efficient saves (only when actual changes occur)
- Proper logging of new vs. updated entities
- Check provider status bar for rate limit consumption (e.g., "4532/5000")
- Hover over status for detailed tooltip with reset time
- Check Logs tab for rate limit messages:
- "Rate limit pacing - waiting Xms"
- "Skipping discovery due to low budget (X% remaining)"
- "Rate limited - resets at HH:mm:ss"
- Adjust thresholds in
BuildProvider.cs:LowBudgetThreshold(currently 15%)CriticalBudgetThreshold(currently 5%)
- Adjust pacing parameters:
BaseRequestDelay(currently 500ms)MinRequestDelay(currently 100ms)- Reserve budget percentage in
CalculateAdaptivePacing()(currently 10%)
- Check
BuildSync.PriorityandRunSync.Priorityproperties - Verify
ShouldUpdateis returning true for the entity - Check if provider's
MaxAllowedPriorityis filtering the entity - Check if entity is orphaned via
IsOrphanedproperty - Verify
UpdateTimeris running and interval has elapsed - Check active requests in
BuildMonitor.ActiveRequestsdictionary - Use "Refresh Build Data" context menu to force immediate update
Key parameters in DurationEstimator.cs:
MinSamplesForEstimate: Minimum successful runs required (currently 3)MaxSamplesToConsider: Maximum recent runs to analyze (currently 20)ExponentialDecayFactor: Weight decay per older sample (currently 0.3 = 70% decay)IqrMultiplier: Outlier detection sensitivity (currently 1.5 = standard Tukey fence)
To favor more recent runs: Increase ExponentialDecayFactor (e.g., 0.5 = 50% decay)
To be more aggressive with outlier removal: Decrease IqrMultiplier (e.g., 1.0)
To use more history: Increase MaxSamplesToConsider