Skip to content

fix(userdata): resolve profile deactivation file conflict - #393

Open
undead2146 wants to merge 3 commits into
community-outpost:developmentfrom
undead2146:fix/user-data-conflict-and-tooling
Open

fix(userdata): resolve profile deactivation file conflict #393
undead2146 wants to merge 3 commits into
community-outpost:developmentfrom
undead2146:fix/user-data-conflict-and-tooling

Conversation

@undead2146

Copy link
Copy Markdown
Member

Summary

Resolves user data file conflicts when a previously active profile is deactivated during profile switching, refactors CasPoolManager path checking helper, and introduces the scripts/build-check.ps1 build mutex script.

Note

This is a self-contained side PR carved out of #265 and must be merged ahead of #265.

Motivation & Problem

  1. When switching profiles, if a previous owner profile is deactivated without unlinking user data, existing file mappings could conflict upon reactivation.
  2. Multiple developer or agent builds colliding on obj/bin locks are serialized safely via a named mutex script.

Changes

  • UserData: Updated ProfileContentLinkerService and UserDataTrackerService to handle prior owner deactivation without file collision.
  • Storage: Cleaned up CasPoolManager with static IsInsideApplicationDirectory method.
  • Tooling: Added scripts/build-check.ps1 with cross-process mutex and Visual Studio debugger detection.
  • Tests: Added comprehensive unit tests in UserDataTrackerServiceTests.cs, cleaned up GameProcessManagerTests.cs and GameInstallationValidatorTests.cs.

Verification

@undead2146 undead2146 changed the title fix(userdata): resolve profile deactivation file conflict and add build-check script fix(userdata): resolve profile deactivation file conflict Aug 19, 2026
@community-outpost community-outpost deleted a comment from coderabbitai Bot Aug 19, 2026
@community-outpost community-outpost deleted a comment from deepsource-io Bot Aug 19, 2026
Comment thread GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs
Comment thread GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs Outdated
Comment thread scripts/build-check.ps1 Outdated
Comment thread scripts/build-check.ps1 Outdated
Comment thread scripts/build-check.ps1 Outdated
@community-outpost community-outpost deleted a comment from qodo-code-review Bot Aug 19, 2026
@community-outpost community-outpost deleted a comment from deepsource-io Bot Aug 19, 2026
@community-outpost community-outpost deleted a comment from qodo-code-review Bot Aug 19, 2026
@community-outpost community-outpost deleted a comment from coderabbitai Bot Aug 19, 2026
@undead2146
undead2146 force-pushed the fix/user-data-conflict-and-tooling branch from 80bbe4f to afb9fa5 Compare August 20, 2026 19:04
@community-outpost community-outpost deleted a comment from deepsource-io Bot Aug 20, 2026
@community-outpost community-outpost deleted a comment from coderabbitai Bot Aug 20, 2026

@kilo-code-bot kilo-code-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review at commit afb9fa5.

if (index.FileToInstallationMap.TryGetValue(normalizedPath, out var installationKey))
{
return OperationResult<string?>.CreateSuccess(installationKey);
var manifest = await LoadUserDataManifestByKeyAsync(installationKey, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: An unreadable manifest is treated the same as a missing one, so a transient read failure can strip an active installation's ownership.

LoadUserDataManifestFromFileAsync (line 1568) returns null both when the manifest file is absent and when reading or deserializing throws (sharing violation, AV/OneDrive lock, corrupt JSON). The new manifest != null && manifest.IsActive guard therefore prunes the FileToInstallationMap entry of a still-active installation on any transient I/O error, and the public CheckFileConflictAsync twin (lines 573-581) persists that prune to disk. Another installation can then claim the path, and a later uninstall of the original manifest removes the new owner's mapping via the blind FileToInstallationMap.Remove in UpdateIndexUnlockedAsync (line 1727) — the same corruption shape the earlier review finding described, now reachable through a locked or corrupt manifest read. DeleteAllUserDataAsync (lines 709-713) already distinguishes these cases with File.Exists(GetManifestFilePath(key)); these conflict paths should do the same and keep the mapping (or return a failure) when the manifest file exists but cannot be read.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

string absolutePath,
CancellationToken cancellationToken = default)
{
await IndexLock.WaitAsync(cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: OperationCanceledException is swallowed and converted into a failure result.

The rewritten CheckFileConflictAsync now performs cancellable awaits inside its try (lock acquisition here, index read at 568, manifest read at 573, index write at 581) but keeps the generic catch (Exception ex) at line 586, so cooperative cancellation surfaces as CreateFailure with a cancelled message. Every other public method in this file rethrows OCE before its generic catch (lines 171, 227, 280, 446, 480, 503 — line 505 even documents why cancellation must escape), and this same PR adds catch (OperationCanceledException) when (...) { throw; } four times in ProfileContentLinkerService.cs. The same gap applies to the new manifest-load await at line 1646 inside CheckFileConflictUnlockedAsync's generic catch, so a cancelled install returns a plain failure (plus a full rollback) from InstallUserDataAsync instead of propagating OCE as its own catch at lines 171-174 promises.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}

// Installation is inactive or manifest no longer exists; clean up stale in-memory mapping
index.FileToInstallationMap.Remove(normalizedPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: The in-memory prune of the shared cached index is not transactionally scoped.

LoadIndexUnlockedAsync returns the _cachedIndex instance itself, so this Remove mutates shared state. If the enclosing install fails before UpdateIndexUnlockedAsync persists (materialization failure, per-file abort, cancellation, or a throw while persisting), nothing rolls the prune back: memory and disk diverge for the rest of the process, and the next unrelated SaveIndexAsync silently makes the prune durable. The public variant (line 581) instead rewrites the entire index file on every stale check through the pre-existing non-atomic File.WriteAllTextAsync path (no temp+rename like SaveUserDataManifestAsync at lines 1538-1543), so a crash mid-write corrupts the index for all future operations. Consider snapshotting prune decisions and persisting them only on successful install, and making SaveIndexAsync atomic.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

.Where(file => file.InstallTarget != ContentInstallTarget.System &&
(file.InstallTarget != ContentInstallTarget.Workspace ||
manifest.ContentType is ContentType.Map or ContentType.MapPack))
.Select(file => (manifest.ContentType is ContentType.Map or ContentType.MapPack) &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Blanket remap of every Workspace file in Map/MapPack manifests into the user Maps directory.

The Select remaps all Workspace-targeted files to UserMapsDirectory, but manifest providers deliberately route non-map payloads to Workspace even for map content — CommunityOutpostManifestFactory sends .big/.ini/.exe/.dll/data/ files to Workspace. Those files are still materialized into the workspace by reconciliation and now also get copied into Documents/.../Maps/; ResolveUserDataTargetPath only strips a leading Maps/ segment (UserDataTrackerService.cs:802), so Data/x.big lands at Maps/Data/x.big and setup.exe at Maps/setup.exe, duplicating bytes (user-writable targets always take full copies) and polluting the folder the game scans for maps. Additionally, same-named Workspace files across two installed packs (for example readme.txt) both resolve to Maps/readme.txt, so the second pack's install hard-fails the entire profile prepare through the conflict abort — a failure mode that did not exist before this remap.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

var userDataFiles = manifest.Files
.Where(f => f.InstallTarget != ContentInstallTarget.Workspace &&
f.InstallTarget != ContentInstallTarget.System)
.Where(file => file.InstallTarget != ContentInstallTarget.System &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Duplicated user-data predicate will drift.

This inline Where predicate is a verbatim copy of HasProfileUserData (lines 377-380). If one copy changes (for example adding ContentType.Mission or filtering by file extension), filtering and installation silently disagree: a manifest can be classified as user data yet install zero files (hitting the empty-success branch at line 424), or files can be installed for manifests the flow skipped. Extract a shared per-file predicate such as IsProfileUserDataFile(manifest, file) and use it in both places.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


// Assert: No conflict reported and stale mapping is pruned
Assert.True(conflictResult.Success);
Assert.Null(conflictResult.Data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: The pruning this test claims to verify is never actually checked.

The test name and comment say the stale mapping is pruned, but only Assert.Null(conflictResult.Data) is asserted — deleting the Remove + SaveIndexAsync lines added at UserDataTrackerService.cs:580-581 (while still returning null) would pass all three new tests, leaving stale mappings to accumulate on disk. Read the persisted index JSON under the fixture's app-data directory and assert the path key is gone.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

{
// Arrange
var gameDataDir = Path.Combine(_zeroHourDataDir, "Maps", "Arabia v2");
Directory.CreateDirectory(gameDataDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Dead arrangement — the pre-created directory is never used.

Directory.CreateDirectory(gameDataDir) creates an empty Maps/Arabia v2 folder that no file is placed in before install A; the install path creates directories itself and no pre-existing-content or backup scenario is set up, so the lines only suggest an arrangement the test never exercises. Remove them, or seed a pre-existing file there if the overwrite path was meant to be covered.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

$"Expected exit failure message, but got: {errors}");
Assert.True(
stopwatch.Elapsed < TimeSpan.FromSeconds(5),
stopwatch.Elapsed < config.ExpectedChildDiscoveryTimeout,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Self-referential timing bound no longer verifies fast failure.

The assertion changed from Elapsed < 5s against a 10s timeout to Elapsed < config.ExpectedChildDiscoveryTimeout while that same timeout was raised to 30s (line 159), so any failure taking up to ~30s now passes while the message still claims Expected a fast failure. The test's documented purpose (FailsWithoutWaitingOutTheTimeoutAsync) is to prove failure is meaningfully faster than the timeout; a bound such as half the timeout keeps that property with CI headroom. Note 30s also no longer mirrors the production default (10s in ProcessConstants).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

File.Delete(tempExe);
}
}
catch (IOException)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Best-effort cleanup can still fail the test.

File.Delete on a just-terminated process's temp script can also throw UnauthorizedAccessException (read-only attribute, ACL or Controlled-Folder-Access interference), which escapes catch (IOException) and fails an otherwise-passing test from its own finally block. The repo's best-effort I/O convention catches both (see GameProcessManager.HasExecutePermission); use a filter like catch (Exception ex) when (ex is IOException or UnauthorizedAccessException).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread .gitignore
!.gitignore
!.agents/
!.claude/
!.gitattributes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Whitelisted .gitattributes does not exist in the repo.

!.gitattributes un-ignores the file, but no .gitattributes exists anywhere in the repository and none is added in this PR, which suggests a companion file (line-ending or linguist rules) was intended but forgotten. Harmless if this is deliberate future-proofing.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 13 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 6
SUGGESTION 7
Issue Details (click to expand)

WARNING

File Line Issue
GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs 1646 Unreadable manifest (transient I/O, corrupt JSON) treated as missing; active installation's ownership pruned and persisted, later uninstall clobbers new owner's mappings
GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs 565 OperationCanceledException swallowed by generic catch in both conflict-check methods, contradicting the file's own OCE-rethrow convention
GenHub/GenHub/Features/UserData/Services/ProfileContentLinkerService.cs 416 Blanket Workspace-to-UserMaps remap for Map/MapPack duplicates non-map payloads (.big/.exe/Data/) into Documents Maps and introduces cross-pack filename collisions that hard-fail profile prepare
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/UserData/UserDataTrackerServiceTests.cs 877 Primary fix test asserts only Success; ownership transfer to profile B never verified
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/UserData/UserDataTrackerServiceTests.cs 966 PrunesStaleMapping claim never verified; deleting the prune+persist lines would pass the suite
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs 172 Self-referential timing bound (elapsed < 30s timeout) no longer verifies fast failure

SUGGESTION

File Line Issue
GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs 1653 In-memory prune of shared cached index is not transactional; diverges from disk on failed installs, and public variant rewrites full index non-atomically per stale check
GenHub/GenHub/Features/UserData/Services/ProfileContentLinkerService.cs 413 User-data predicate duplicated between HasProfileUserData and inline filter; drift risk
GenHub/GenHub/Features/UserData/Services/ProfileContentLinkerService.cs 424 Fabricated empty UserDataManifest success payload; inconsistent with tracker's failure for empty input; stale <returns> doc
GenHub/GenHub/Features/UserData/Services/ProfileContentLinkerService.cs 375 Static helpers placed after instance methods, violating coding-style.md section 4
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/UserData/UserDataTrackerServiceTests.cs 837 Dead Directory.CreateDirectory arrangement never exercised
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs 403 Best-effort cleanup catches only IOException; UnauthorizedAccessException can still fail the test
.gitignore 10 !.gitattributes whitelists a file that does not exist in the repo
Files Reviewed (7 files)
  • GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs - 3 issues
  • GenHub/GenHub/Features/UserData/Services/ProfileContentLinkerService.cs - 4 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/UserData/UserDataTrackerServiceTests.cs - 3 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs - 2 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/GameInstallationValidatorTests.cs - no issues
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs - no issues (behavior-preserving static move)
  • .gitignore - 1 issue

Fix these issues in Kilo Cloud


Reviewed by glm-5.3 · Input: 106.1K · Output: 39.3K · Cached: 2.2M

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