Repository navigation
Harden removal, async lifecycles, cache, and logging - #12
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Removal retains a path-based escape race, and cache invalidation can outlive shutdown cleanup.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Hardens destructive removal, asynchronous TUI lifecycles, shared caches, and bounded logging.
Changes:
- Adds safer removal traversal and single-flight cache/probe operations.
- Tracks background tasks and introduces workspace-scoped cancellation.
- Adds exact-once log replay, memory limits, file rotation, and regression tests.
File summaries
| File | Description |
|---|---|
AGENTS.md |
Documents new lifecycle, removal, and logging rules. |
docs/reviews/adversarial-concurrency-resource-cancellation-audit-2026-08-28.md |
Records audit findings and remediation status. |
src/SkillView.Core/Gh/GhSkillInstallService.cs |
Bounds logged installation errors. |
src/SkillView.Core/Gh/GhSkillListAdapter.cs |
Uses single-flight cache loading. |
src/SkillView.Core/Gh/GhSkillListCache.cs |
Synchronizes caching, loading, and invalidation. |
src/SkillView.Core/Gh/GhSkillSearchService.cs |
Bounds logged search errors. |
src/SkillView.Core/Gh/GhSkillUpdateService.cs |
Bounds logged update errors. |
src/SkillView.Core/Inventory/RemoveService.cs |
Adds bounded, link-aware removal traversal. |
src/SkillView.Core/Logging/FileLogSink.cs |
Adds replay attachment and size rotation. |
src/SkillView.Core/Logging/Logger.cs |
Adds retention budgets and ordered replay. |
src/SkillView.Core/Logging/LogPaths.cs |
Supports numbered log parts. |
src/SkillView.Core/Ui/BackgroundTaskTracker.cs |
Tracks and drains owned background work. |
src/SkillView.Core/Ui/SharedAsyncOperation.cs |
Shares cancellable concurrent operations. |
src/SkillView.Core/Ui/SkillViewApp.cs |
Integrates task ownership and workspace lifetimes. |
src/SkillView.Core/Ui/SkillViewWorkflowCoordinator.cs |
Shares environment probes across workflows. |
src/SkillView.Core/Ui/Tabs/InstalledTabView.cs |
Routes deferred work through task tracking. |
src/SkillView.Core/Ui/Tabs/UpdatesTabView.cs |
Tracks update and loading operations. |
tests/SkillView.Tests/Gh/GhSkillListCacheTests.cs |
Covers cache concurrency and cancellation. |
tests/SkillView.Tests/Inventory/RemoveServiceTests.cs |
Covers links, junctions, cycles, and cancellation. |
tests/SkillView.Tests/Logging/FileLogSinkTests.cs |
Covers replay, rotation, and retention. |
tests/SkillView.Tests/Logging/LoggerTests.cs |
Covers bounded retention and ordered replay. |
tests/SkillView.Tests/Ui/BackgroundTaskTrackerTests.cs |
Covers task admission, draining, and faults. |
tests/SkillView.Tests/Ui/InstalledTabViewTests.cs |
Updates task-runner setup. |
tests/SkillView.Tests/Ui/SharedAsyncOperationTests.cs |
Covers shared execution and cleanup. |
tests/SkillView.Tests/Ui/SkillViewAppTests.cs |
Covers workspace and shutdown lifecycles. |
tests/SkillView.Tests/Ui/UpdatesTabViewTests.cs |
Updates tracked-task test setup. |
Review details
- Files reviewed: 26/26 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Stale previews can overwrite new results, log ordering can retain unbounded pending entries, and parsing failures are cached as empty inventory.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/SkillView.Core/Ui/SkillViewApp.cs:1162
- A preview can still become stale if it is started from the old table while this search is in flight: the search only cancels previews before awaiting, so that later preview remains current and can overwrite the pane after these new results are installed. Cancel the preview gate again when committing successful search results so any preview launched against the superseded result set cannot paint afterward.
if (!request.IsCurrent
|| !IsDiscoverWorkspaceActive(discoverGeneration)
|| System.Threading.Interlocked.Read(ref _searchGeneration) != generation)
src/SkillView.Core/Gh/GhSkillListAdapter.cs:93
- Malformed or unexpected successful output is still cached as a valid empty result because
Parsereports failure by returning an empty array. A truncated JSON payload or schema mismatch therefore suppresses retries for the TTL and can hideghinventory records. Preserve a parse-success signal and setShouldCache: falsewhen parsing fails; only a genuinely valid empty payload should be cached.
var parsed = Parse(result.StdOut, _logger);
return new GhSkillListCache.LoadResult(parsed);
- Files reviewed: 26/26 changed files
- Comments generated: 1
- Review effort level: Balanced
|
Addressed the two suppressed findings from the latest Copilot review in a982a50 as first-class review issues:
The full suite passes: 602 tests, 0 failures; build passes with 0 warnings and 0 errors. The audit, AGENTS.md, lifecycle guidance, and PR release-note description have been updated. |
There was a problem hiding this comment.
🟡 Changes recommended
Queued search and preview callbacks can outlive their request leases, causing valid UI updates to be discarded.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/SkillView.Core/Ui/SkillViewApp.cs:1994
- Canceling the preview with
clearBusy: truecan clear a still-running search's shared spinner. A search permits a preview from the old table while it is in flight, so opening logs in that state cancels the preview and leaves the ongoing search with no busy indication. Track busy ownership (or restore the search busy state) instead of unconditionally clearing the global indicator for the canceled preview.
// Logs are an explicit user choice. A preview already in flight must
// not be allowed to close the log pane when it completes.
CancelCurrentPreview(clearBusy: true);
src/SkillView.Core/Ui/SkillViewApp.cs:1343
- This callback has the same lease-lifetime race: after
ConfigureAwait(false),Invokequeues the action and returns, thenPreviewSelectedAsyncdisposesrequest. A callback delayed until the next UI-loop tick observesrequest.IsCurrent == falseand silently discards a valid preview. Keep the lease alive until an awaitable UI dispatch completes; update the timeout/error/finally dispatches consistently.
Invoke(() =>
{
if (!request.IsCurrent || !IsDiscoverWorkspaceActive(discoverGeneration)) return;
- Files reviewed: 28/28 changed files
- Comments generated: 1
- Review effort level: Balanced
|
Also addressed the latest review summary’s suppressed busy-state finding in 78efa90. The status spinner is no longer a single ownerless boolean for overlapping search and preview work. Each operation registers a monotonic busy owner; ending or canceling one removes only that owner and restores the newest remaining operation. Opening logs while an old-table preview and search overlap therefore cancels the preview but retains the search spinner and exact search status text. Workspace deactivation retains an explicit clear-all operation. CancelingPreview_RestoresStillRunningSearchBusyState exercises the real log-pane path deterministically. Validation: warning-free build; 605 tests passed; audit, AGENTS.md, lifecycle guidance, PR description, and release notes updated. |
There was a problem hiding this comment.
🟡 Changes recommended
Cache schema validation, logger recursion handling, and shutdown cancellation reporting have unresolved correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 28/28 changed files
- Comments generated: 3
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Disposed-token races and a cross-thread logger cycle can still cause teardown failures or deadlocks.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/SkillView.Core/Ui/SkillViewApp.cs:1196
- Workspace deactivation cancels and disposes
_discoverLifetime, but this continuation dereferencesdiscoverLifetime.Tokenafter awaiting the search. If tab navigation wins the race after the subprocess returns, this throwsObjectDisposedExceptionbefore the ownership check can reject the stale callback, and the tracked task is reported as a crash. Capture theCancellationTokenvalue before the first await and use that value for every dispatch in this method.
This issue also appears on line 1372 of the same file.
}, discoverLifetime.Token).ConfigureAwait(false);
src/SkillView.Core/Ui/SkillViewApp.cs:1372
- This has the same disposed-source race as search: leaving Discover disposes the captured
CancellationTokenSource, so a preview that returns concurrently can throw while evaluatingdiscoverLifetime.Token; the generic catch then dereferences it again and lets the exception escape toBackgroundTaskTracker. Capture the token struct before awaiting and use it for all preview dispatches.
}, discoverLifetime.Token).ConfigureAwait(false);
- Files reviewed: 28/28 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Aggregate log usage can exceed the configured disk budget until the next file rotation.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/SkillView.Core/Logging/FileLogSink.cs:125
- The total disk budget is only checked on the first append after opening or rotating a writer. After that
_trimPendingis false while the active part can grow by almost 5 MiB, so previously retained parts can remain even when active growth pushes aggregate usage above_totalSizeBudgetBytes. Re-run budget enforcement as the active file grows (ideally with incremental accounting or a threshold to avoid enumerating files for every line), and add a test where old parts initially fit but later appends force their removal.
if (_trimPending)
{
_trimPending = false;
TrimLocked();
- Files reviewed: 28/28 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Summary
This is the first implementation checkpoint from the repository-wide adversarial concurrency, cancellation, resource-usage, and cross-platform audit. It completes the first six recommended remediations and includes all five rounds of Copilot review fixes with deterministic regression coverage.
The changes focus on preventing destructive filesystem escapes, duplicate subprocess work, late UI mutation after teardown, stale workspace callbacks and previews, premature request-lease disposal, cross-operation spinner clearing, lost or duplicated log entries, malformed inventory caching, and unbounded log memory/disk growth.
What changed
1. Safe removal traversal
This closes the static packaged-link path where a nested link could allow removal to traverse outside the selected skill directory. It does not claim atomic protection against a hostile same-user process replacing a path component between validation and a path-based delete; supported .NET 10 APIs do not expose portable handle-relative/no-follow deletion. That native cross-platform design remains in the removal follow-up and is documented in the audit.
2. Thread-safe, single-flight
gh skill listcachegh skill listprocess.3. Owned background work and shutdown quiescence
BackgroundTaskTrackerto reserve and track application-owned background work before it starts.CRASH; unrelated cancellation and faults still surface.4. Discover and Doctor workspace lifetimes
5. Exact-once log replay and subscription
Logger.SubscribeWithReplayto establish an atomic retained-history/live-entry boundary.FileLogSinknow use the shared replay primitive instead of separate snapshot-then-subscribe protocols.This resolves both the visible-log replacement race highlighted in PR #11 and the analogous disk-sink gap discovered during reassessment.
6. Bounded logging memory and disk usage
ghadapters.Cross-platform behavior
mklink /J; it executes on the repository's Windows CI runner.Validation
dotnet build --no-restore— passed with zero warnings and zero errors.dotnet test --no-build— 613 passed, 0 failed, including the ANSI-driver integration suite.git diff --check— passed.The optional, non-configured
AnalysisLevel=latest-allruleset currently reports 190 warnings-as-errors across changed and unchanged repository code. It requires a separate baseline and triage before it can serve as a meaningful regression gate; the configured repository build remains clean.Remaining audit work
This PR intentionally stops at a cohesive, independently reviewable checkpoint. The next remediation items remain documented and will be handled separately:
Documentation
docs/reviews/adversarial-concurrency-resource-cancellation-audit-2026-08-28.mdwith the complete findings, evidence, all five Copilot-review reassessments, remediation status, remaining order, and Terminal.Gui upstream opportunities.AGENTS.mdandagent_docs/ui-lifecycle-and-resource-bounds.mdwith durable rules for task ownership, workspace generations, safe removal traversal, exact-once bounded log delivery, parse-aware caching, and bounded logging.Release-note summary
gh skill listsubprocesses and cache races.