Skip to content

Fix concurrent lane deletes#384

Merged
arul28 merged 1 commit into
mainfrom
ade/creating-lane-while-lane-deletes-615cb70d
May 28, 2026
Merged

Fix concurrent lane deletes#384
arul28 merged 1 commit into
mainfrom
ade/creating-lane-while-lane-deletes-615cb70d

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 28, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

ADE   Open in ADE  ·  ade/creating-lane-while-lane-deletes-615cb70d branch  ·  PR #384

Summary by CodeRabbit

  • New Features

    • Lane deletions now execute concurrently (up to 2 at a time), improving deletion speed when removing multiple lanes.
    • iOS app now organizes lane deletions by dependency relationships for more predictable processing.
  • Bug Fixes

    • Enhanced error handling in lane deletion workflows for improved failure recovery.
  • Tests

    • Added coverage for concurrent lane deletion scenarios and dependency-based batch processing.

Review Change Stack

Greptile Summary

This PR narrows the worktree-mutation guard in the lane delete pipeline so that only git_worktree_remove serializes through the shared queue, allowing concurrent lane deletes to progress through process/PTY/watcher teardown independently and unblocking lane creation that would previously wait for the entire delete to finish. The batch delete runner on both desktop and iOS is upgraded from sequential (one-at-a-time) to a limited-concurrency model (2 at a time, dependency-order-safe).

  • Desktop (laneService.ts): Removes the outer runGitWorktreeMutation wrapper from delete; the guard is retained only around the git_worktree_remove step, so concurrent deletes share only the narrow Git registry mutation window.
  • Desktop (lanePageModel.ts): runLaneDeleteBatchSequentially is replaced by runLaneDeleteBatchWithConcurrency, a work-stealing pool that dispatches up to 2 deletions at a time and preserves per-lane failure results.
  • iOS (LaneBatchManageSheet.swift): Adds laneDeleteDependencyBatches (topological sort) and runLaneDeleteBatchWithConcurrency (chunked async let pairs); replaces the old sequential for-loop delete with the new batched concurrent dispatch.

Confidence Score: 4/5

Safe to merge — the core locking invariant is preserved (only git_worktree_remove enters the shared worktree-mutation queue) and the TypeScript work-stealing pool is correct at any concurrency level. The Swift implementation has a structural assumption baked in that needs attention if the concurrency constant is ever raised.

The architectural change on the desktop side is well-reasoned and well-tested. The Swift runLaneDeleteBatchWithConcurrency hard-codes async let first / async let second, which silently breaks if laneDeleteBatchConcurrency is increased beyond 2. The 40 ms fixed-sleep assertion in the new concurrency test is another point of potential intermittent failure on CI. Neither is a blocking correctness issue in the current state, but they deserve attention before the constant or test infrastructure changes.

apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift (chunk handling is hardcoded for 2 items) and apps/desktop/src/main/services/lanes/laneService.test.ts (fixed 40 ms sleep for concurrency assertion).

Important Files Changed

Filename Overview
apps/desktop/src/main/services/lanes/laneService.ts Removes the outer runGitWorktreeMutation guard from the delete pipeline, leaving it only around the git_worktree_remove step; independent lane deletes can now proceed through stop_processes, PTYs, watchers, and env cleanup concurrently without holding the worktree-mutation slot.
apps/desktop/src/renderer/components/lanes/lanePageModel.ts Replaces runLaneDeleteBatchSequentially with runLaneDeleteBatchWithConcurrency using a work-stealing pool pattern; adds LANE_DELETE_BATCH_CONCURRENCY = 2 constant; implementation correctly handles any concurrency level.
apps/desktop/src/main/services/lanes/laneService.test.ts Updates mock signature for stopAll, adds a new concurrency test, and rewrites the lane-creation-during-delete test to assert the new allowed-concurrent behavior; uses fixed 40 ms timeout for the new concurrency assertion which may be flaky on loaded CI.
apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift Adds laneDeleteDependencyBatches, runLaneDeleteBatchWithConcurrency, and runLaneDeleteOperation; replaces sequential for-loop delete with batched concurrent dispatch; the concurrent runner hard-codes handling for at most 2 items per chunk (safe now, fragile if constant increases).
apps/desktop/src/renderer/components/lanes/LanesPage.tsx Trivial rename from runLaneDeleteBatchSequentially to runLaneDeleteBatchWithConcurrency at both the import site and the single call site; no logic changes.
apps/desktop/src/renderer/components/lanes/LanesPage.test.ts Test renamed and updated to verify that two deletes start concurrently (maxActive=2) rather than one at a time, and that a third lane is picked up after the first wave completes.
apps/ios/ADETests/ADETests.swift Adds LaneBatchDeleteRecorder actor for thread-safe concurrency tracking, testLaneDeleteDependencyBatchesKeepAncestorsAfterSelectedDescendants, and testLaneDeleteBatchRunnerStartsTwoDeletesAtOnceAndPreservesOrder; tests look correct for the current fixed-chunk Swift implementation.
docs/features/lanes/README.md Documentation updated to reflect the narrowed worktree-mutation guard scope and the new concurrency model; accurate and consistent with the code changes.
docs/features/lanes/worktree-isolation.md Expanded teardown step list and clarified that independent deletes share only the worktree-registry guard; documentation accurately mirrors the implementation.

Sequence Diagram

sequenceDiagram
    participant UI as LanesPage
    participant Batch as runLaneDeleteBatchWithConcurrency
    participant D1 as delete lane-child
    participant D2 as delete lane-sibling
    participant Mut as worktree-mutation guard
    participant C as create new-lane

    UI->>Batch: batch with lane-child and lane-sibling
    par Worker 1
        Batch->>D1: start teardown
        D1->>D1: git_status, stop_processes, PTYs, watchers
    and Worker 2
        Batch->>D2: start teardown
        D2->>D2: git_status, stop_processes, PTYs, watchers
    end
    Note over D1,D2: teardown runs concurrently, no guard held

    UI->>C: create new-lane
    C->>Mut: acquire slot
    Mut-->>C: granted
    C->>C: git worktree add
    Mut-->>C: released

    D1->>Mut: acquire slot
    Mut-->>D1: granted
    D1->>D1: git worktree remove
    Mut-->>D1: released
    D1->>D1: database_cleanup

    D2->>Mut: acquire slot
    Mut-->>D2: granted
    D2->>D2: git worktree remove
    Mut-->>D2: released
    D2->>D2: database_cleanup
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/main/services/lanes/laneService.test.ts, line 60-67 (link)

    P2 Fixed 40 ms wait is flaky on loaded CI

    setTimeout(resolve, 40) is used to assert that both deletes have entered stop_processes before any worktree removal begins. On a saturated CI machine the two async tasks may not have reached that checkpoint within 40 ms, causing the expect([...startedStops].sort()).toEqual(...) assertion to fail intermittently. The sibling "allows lane creation…" test already adopts a polling loop (up to 10 × 5 ms) for the same class of timing assertion — applying that pattern here (or using the gate/promise approach used by stopStartedPromise elsewhere in the suite) would remove the fixed-timeout dependency.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/lanes/laneService.test.ts
    Line: 60-67
    
    Comment:
    **Fixed 40 ms wait is flaky on loaded CI**
    
    `setTimeout(resolve, 40)` is used to assert that both deletes have entered `stop_processes` before any worktree removal begins. On a saturated CI machine the two async tasks may not have reached that checkpoint within 40 ms, causing the `expect([...startedStops].sort()).toEqual(...)` assertion to fail intermittently. The sibling "allows lane creation…" test already adopts a polling loop (up to 10 × 5 ms) for the same class of timing assertion — applying that pattern here (or using the gate/promise approach used by `stopStartedPromise` elsewhere in the suite) would remove the fixed-timeout dependency.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift:57-63
**`else` branch hard-codes exactly two concurrent items**

The `else` branch explicitly references only `chunk[0]` and `chunk[1]`. Because `normalizedConcurrency` is clamped to `laneDeleteBatchConcurrency` (currently 2), chunks can never exceed two items today, but if the constant is bumped to 3 or higher the function will silently stop processing `chunk[2]` (and beyond) — delivering results for only the first two items in each chunk and leaving the rest untouched. The TypeScript equivalent uses a proper work-stealing pool (`Promise.all` over N workers) that stays correct at any concurrency level. Consider using `withTaskGroup` here so the implementation generalises cleanly without an invisible length constraint.

### Issue 2 of 3
apps/desktop/src/main/services/lanes/laneService.test.ts:60-67
**Fixed 40 ms wait is flaky on loaded CI**

`setTimeout(resolve, 40)` is used to assert that both deletes have entered `stop_processes` before any worktree removal begins. On a saturated CI machine the two async tasks may not have reached that checkpoint within 40 ms, causing the `expect([...startedStops].sort()).toEqual(...)` assertion to fail intermittently. The sibling "allows lane creation…" test already adopts a polling loop (up to 10 × 5 ms) for the same class of timing assertion — applying that pattern here (or using the gate/promise approach used by `stopStartedPromise` elsewhere in the suite) would remove the fixed-timeout dependency.

### Issue 3 of 3
apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift:40-68
**Chunked fixed-size batching vs. TypeScript work-stealing pool**

The Swift implementation divides `laneIds` into sequential chunks and waits for every item in a chunk to complete before starting the next. The TypeScript `runLaneDeleteBatchWithConcurrency` uses a work-stealing pool: a worker picks up the next item as soon as it finishes its current one. With concurrency=2 and three lanes, if lane-a finishes much faster than lane-b, TypeScript will start lane-c immediately while lane-b is still running, but the Swift version waits for lane-b before lane-c starts. This is unlikely to cause user-visible issues at concurrency=2, but documents a behavioural divergence worth tracking if either constant changes.

Reviews (1): Last reviewed commit: "Fix concurrent lane deletes" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored May 28, 2026 3:42pm

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 28, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

This PR implements concurrent batch deletion of lanes across desktop and iOS platforms. It introduces concurrency helpers with configurable parallelism limits, restructures the main service delete flow to allow concurrent teardowns, and wires the new execution model into both UI layers while preserving per-lane error tracking.

Changes

Concurrent Lane Deletion

Layer / File(s) Summary
Desktop concurrency infrastructure
apps/desktop/src/renderer/components/lanes/lanePageModel.ts
Introduces LANE_DELETE_BATCH_CONCURRENCY constant and runLaneDeleteBatchWithConcurrency<T> helper that distributes lane deletion work across multiple async workers, recording per-lane results by original position before returning the aggregated results array.
Main service delete step orchestration
apps/desktop/src/main/services/lanes/laneService.ts, apps/desktop/src/main/services/lanes/laneService.test.ts
Restructures laneService.delete() to permit concurrent independent teardowns by removing the outer runGitWorktreeMutation wrapper, applying mutation-serialization only to the git_worktree_remove step. Tests verify concurrent teardown gates and that creation can interleave with concurrent in-flight deletes.
Desktop UI integration and testing
apps/desktop/src/renderer/components/lanes/LanesPage.tsx, apps/desktop/src/renderer/components/lanes/LanesPage.test.ts
Switches deleteManagedLanes to invoke runLaneDeleteBatchWithConcurrency instead of sequential execution; test validates max 2 concurrent deletes, first-wave parallel start, and error preservation across lanes.
iOS concurrency infrastructure and helpers
apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift
Adds laneDeleteDependencyBatches(snapshots:) to group selected lanes into batches ordered by parent/child dependencies, runLaneDeleteBatchWithConcurrency to chunk and execute deletions concurrently with configurable parallelism, and runSingleLaneDelete wrapper to convert thrown errors into LaneBatchOperationResult values.
iOS integration and comprehensive testing
apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift, apps/ios/ADETests/ADETests.swift
Refactors deleteSelected() to replace sequential iteration with dependency-batch-driven concurrent execution, aggregating per-lane success/failure results. Adds LaneBatchDeleteRecorder actor for deterministic test coordination, and tests verifying dependency ordering and concurrent parallelism with error propagation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • arul28/ADE#289: Both PRs alter concurrency and ordering in the lane deletion workflow, touching laneService.ts and lane deletion batching logic in desktop and iOS components.
  • arul28/ADE#290: Addresses non-fatal step failures and warning surfacing in the lane deletion flow, complementing the main PR's concurrency-focused changes to laneService.delete() and delete status handling.

Suggested labels

desktop, ios

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix concurrent lane deletes' directly aligns with the core changeset focus: introducing concurrent execution (via new runLaneDeleteBatchWithConcurrency functions) across multiple files, replacing sequential deletion patterns, and adding comprehensive tests validating parallel lane deletion behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/creating-lane-while-lane-deletes-615cb70d

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 28, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

PR Review

Scope: 9 file(s), +391 / −89
Verdict: Looks good

This PR lets lane creation and most delete teardown steps proceed while another lane is deleting, by removing the outer runGitWorktreeMutation wrapper around the full delete pipeline and keeping that guard only around git worktree remove. Desktop batch deletes and iOS batch manage now run up to two independent lane deletes per dependency-safe batch, with tests and docs updated to match.


Notes

  • The backend change is narrowly scoped: laneService.delete still rejects duplicate in-flight deletes for the same lane, still blocks deleting a lane that has active child rows in the DB, and still serializes actual worktree registry mutations via runGitWorktreeMutation at git_worktree_remove (laneService.ts ~4222–4260). New tests cover concurrent deletes and create-during-teardown ordering.
  • Desktop runLaneDeleteBatchWithConcurrency uses a small worker pool that can start the next lane as soon as a slot frees; iOS runLaneDeleteBatchWithConcurrency uses fixed pairs of chunks, so a third lane in the same batch may wait for the whole first pair to finish. That is a minor scheduling difference, not a correctness gap.
  • iOS batch delete still does not mirror desktop's "skip parent when a selected child delete failed" handling in LanesPage.tsx; that parity gap predates this PR and is unchanged by the batching refactor.
Open in Web View Automation 

Sent by Cursor Automation: BUGBOT in Versic

Copy link
Copy Markdown
Contributor

Copilot AI commented May 28, 2026

@copilot review but do not make fixes

Reviewed the latest commit and didn’t find any blocking issues to fix. I have not made any code changes.

Copilot finished work on behalf of arul28 May 28, 2026 15:47
Comment on lines +57 to +63
if chunk.count == 1 {
results.append(await runLaneDeleteOperation(laneId: chunk[0], operation: operation))
} else {
async let first = runLaneDeleteOperation(laneId: chunk[0], operation: operation)
async let second = runLaneDeleteOperation(laneId: chunk[1], operation: operation)
results.append(contentsOf: await [first, second])
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 else branch hard-codes exactly two concurrent items

The else branch explicitly references only chunk[0] and chunk[1]. Because normalizedConcurrency is clamped to laneDeleteBatchConcurrency (currently 2), chunks can never exceed two items today, but if the constant is bumped to 3 or higher the function will silently stop processing chunk[2] (and beyond) — delivering results for only the first two items in each chunk and leaving the rest untouched. The TypeScript equivalent uses a proper work-stealing pool (Promise.all over N workers) that stays correct at any concurrency level. Consider using withTaskGroup here so the implementation generalises cleanly without an invisible length constraint.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift
Line: 57-63

Comment:
**`else` branch hard-codes exactly two concurrent items**

The `else` branch explicitly references only `chunk[0]` and `chunk[1]`. Because `normalizedConcurrency` is clamped to `laneDeleteBatchConcurrency` (currently 2), chunks can never exceed two items today, but if the constant is bumped to 3 or higher the function will silently stop processing `chunk[2]` (and beyond) — delivering results for only the first two items in each chunk and leaving the rest untouched. The TypeScript equivalent uses a proper work-stealing pool (`Promise.all` over N workers) that stays correct at any concurrency level. Consider using `withTaskGroup` here so the implementation generalises cleanly without an invisible length constraint.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +40 to +68
@MainActor
func runLaneDeleteBatchWithConcurrency<Value>(
laneIds: [String],
concurrency: Int = laneDeleteBatchConcurrency,
operation: @escaping (String) async throws -> Value
) async -> [LaneBatchOperationResult<Value>] {
guard !laneIds.isEmpty else { return [] }

let normalizedConcurrency = max(1, min(laneIds.count, min(concurrency, laneDeleteBatchConcurrency)))
var results: [LaneBatchOperationResult<Value>] = []
results.reserveCapacity(laneIds.count)

var index = 0
while index < laneIds.count {
let nextIndex = min(index + normalizedConcurrency, laneIds.count)
let chunk = Array(laneIds[index..<nextIndex])

if chunk.count == 1 {
results.append(await runLaneDeleteOperation(laneId: chunk[0], operation: operation))
} else {
async let first = runLaneDeleteOperation(laneId: chunk[0], operation: operation)
async let second = runLaneDeleteOperation(laneId: chunk[1], operation: operation)
results.append(contentsOf: await [first, second])
}

index = nextIndex
}

return results
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Chunked fixed-size batching vs. TypeScript work-stealing pool

The Swift implementation divides laneIds into sequential chunks and waits for every item in a chunk to complete before starting the next. The TypeScript runLaneDeleteBatchWithConcurrency uses a work-stealing pool: a worker picks up the next item as soon as it finishes its current one. With concurrency=2 and three lanes, if lane-a finishes much faster than lane-b, TypeScript will start lane-c immediately while lane-b is still running, but the Swift version waits for lane-b before lane-c starts. This is unlikely to cause user-visible issues at concurrency=2, but documents a behavioural divergence worth tracking if either constant changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift
Line: 40-68

Comment:
**Chunked fixed-size batching vs. TypeScript work-stealing pool**

The Swift implementation divides `laneIds` into sequential chunks and waits for every item in a chunk to complete before starting the next. The TypeScript `runLaneDeleteBatchWithConcurrency` uses a work-stealing pool: a worker picks up the next item as soon as it finishes its current one. With concurrency=2 and three lanes, if lane-a finishes much faster than lane-b, TypeScript will start lane-c immediately while lane-b is still running, but the Swift version waits for lane-b before lane-c starts. This is unlikely to cause user-visible issues at concurrency=2, but documents a behavioural divergence worth tracking if either constant changes.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/desktop/src/main/services/lanes/laneService.test.ts (1)

3003-3059: 💤 Low value

Consider polling instead of a fixed 40ms timeout to reduce CI flakiness.

Line 3048 uses setTimeout(resolve, 40) to wait for both stopAll calls to start. In slow CI environments, this fixed delay could occasionally be insufficient. A polling approach (similar to lines 3104-3106) would be more deterministic:

-    await new Promise((resolve) => setTimeout(resolve, 40));
-
-    expect([...startedStops].sort()).toEqual(["lane-child", "lane-sibling"]);
+    for (let i = 0; i < 20 && startedStops.size < 2; i += 1) {
+      await new Promise((r) => setTimeout(r, 5));
+    }
+    expect([...startedStops].sort()).toEqual(["lane-child", "lane-sibling"]);

This completes as soon as both stops start rather than always waiting the full duration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/lanes/laneService.test.ts` around lines 3003 -
3059, Replace the fixed sleep after starting deletePromises with a polling loop
that waits until fake.processService.stopAll has been invoked for both lanes;
specifically, instead of awaiting new Promise(resolve => setTimeout(resolve,
40)), poll the startedStops Set (populated by the mocked stopAll) until it
contains "lane-child" and "lane-sibling" (with a short delay per iteration and a
reasonable overall timeout to avoid hangs) before releasing stops and awaiting
deletePromises; keep references to service.delete, fake.processService.stopAll,
startedStops, deletePromises, and releaseStops/stopGate to locate where to
change the wait.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift`:
- Around line 57-63: The current async-let code only handles exactly two items
and will drop extra IDs when laneDeleteBatchConcurrency/normalizedConcurrency >
2; replace the hardcoded async-let block with a dynamic TaskGroup over the
chunk: iterate chunk, add each runLaneDeleteOperation(laneId: id, operation:
operation) as a child task, await group results and append them to results. If
runLaneDeleteOperation is `@MainActor`, either remove/main-actor-isolate that
function or run each child task body as Task { await MainActor.run { await
runLaneDeleteOperation(...) } } so the TaskGroup can schedule children
dynamically without losing actor correctness.

In `@apps/ios/ADETests/ADETests.swift`:
- Around line 5100-5104: The test currently asserts strict ordering with
XCTAssertEqual(firstStartedIds, ["lane-a", "lane-b"]) which can flake due to
scheduler timing; change this to an order-insensitive assertion (e.g., compare
Set(firstStartedIds) to Set(["lane-a", "lane-b"]) or assert both expected IDs
are present regardless of order) while keeping the max active count check
(firstMaxActiveCount) intact; update the assertion that references
recorder.startedIds(), firstStartedIds and the XCTAssertEqual call accordingly.

---

Nitpick comments:
In `@apps/desktop/src/main/services/lanes/laneService.test.ts`:
- Around line 3003-3059: Replace the fixed sleep after starting deletePromises
with a polling loop that waits until fake.processService.stopAll has been
invoked for both lanes; specifically, instead of awaiting new Promise(resolve =>
setTimeout(resolve, 40)), poll the startedStops Set (populated by the mocked
stopAll) until it contains "lane-child" and "lane-sibling" (with a short delay
per iteration and a reasonable overall timeout to avoid hangs) before releasing
stops and awaiting deletePromises; keep references to service.delete,
fake.processService.stopAll, startedStops, deletePromises, and
releaseStops/stopGate to locate where to change the wait.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 71ecbab2-b78f-4e2f-9d41-bd477a3bbb52

📥 Commits

Reviewing files that changed from the base of the PR and between a3c6e76 and dfe7127.

⛔ Files ignored due to path filters (2)
  • docs/features/lanes/README.md is excluded by !docs/**
  • docs/features/lanes/worktree-isolation.md is excluded by !docs/**
📒 Files selected for processing (7)
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/renderer/components/lanes/LanesPage.test.ts
  • apps/desktop/src/renderer/components/lanes/LanesPage.tsx
  • apps/desktop/src/renderer/components/lanes/lanePageModel.ts
  • apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift
  • apps/ios/ADETests/ADETests.swift
💤 Files with no reviewable changes (1)
  • apps/desktop/src/main/services/lanes/laneService.ts

Comment on lines +57 to +63
if chunk.count == 1 {
results.append(await runLaneDeleteOperation(laneId: chunk[0], operation: operation))
} else {
async let first = runLaneDeleteOperation(laneId: chunk[0], operation: operation)
async let second = runLaneDeleteOperation(laneId: chunk[1], operation: operation)
results.append(contentsOf: await [first, second])
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hardcoded concurrency handling will silently drop items if constant is changed.

The async let pattern here only handles exactly 2 items (chunk[0] and chunk[1]), but the chunk size is determined by normalizedConcurrency. If laneDeleteBatchConcurrency is increased to a value > 2, the chunk will contain more items than the code processes, silently dropping deletions.

Use TaskGroup for dynamic concurrency or remove the configurable parameter and explicitly document the hard limit of 2.

Proposed fix using TaskGroup for flexible concurrency
-    if chunk.count == 1 {
-      results.append(await runLaneDeleteOperation(laneId: chunk[0], operation: operation))
-    } else {
-      async let first = runLaneDeleteOperation(laneId: chunk[0], operation: operation)
-      async let second = runLaneDeleteOperation(laneId: chunk[1], operation: operation)
-      results.append(contentsOf: await [first, second])
-    }
+    let chunkResults = await withTaskGroup(of: LaneBatchOperationResult<Value>.self) { group in
+      for laneId in chunk {
+        group.addTask {
+          await runLaneDeleteOperation(laneId: laneId, operation: operation)
+        }
+      }
+      var collected: [LaneBatchOperationResult<Value>] = []
+      for await result in group {
+        collected.append(result)
+      }
+      return collected
+    }
+    results.append(contentsOf: chunkResults)

Note: runLaneDeleteOperation would need to drop @MainActor or be called differently since TaskGroup child tasks inherit the actor context but can't use @MainActor functions directly in this pattern. Consider whether main-actor isolation is actually needed for the operation wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/Lanes/LaneBatchManageSheet.swift` around lines 57 - 63,
The current async-let code only handles exactly two items and will drop extra
IDs when laneDeleteBatchConcurrency/normalizedConcurrency > 2; replace the
hardcoded async-let block with a dynamic TaskGroup over the chunk: iterate
chunk, add each runLaneDeleteOperation(laneId: id, operation: operation) as a
child task, await group results and append them to results. If
runLaneDeleteOperation is `@MainActor`, either remove/main-actor-isolate that
function or run each child task body as Task { await MainActor.run { await
runLaneDeleteOperation(...) } } so the TaskGroup can schedule children
dynamically without losing actor correctness.

Comment on lines +5100 to +5104
await recorder.waitForStartedCount(2)
let firstStartedIds = await recorder.startedIds()
let firstMaxActiveCount = await recorder.maxActiveCount()
XCTAssertEqual(firstStartedIds, ["lane-a", "lane-b"])
XCTAssertEqual(firstMaxActiveCount, 2)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid strict ordering assertions for concurrently started tasks.

firstStartedIds can be ["lane-b", "lane-a"] depending on scheduler timing, so this assertion may flake while concurrency is still correct.

Suggested test hardening
-    XCTAssertEqual(firstStartedIds, ["lane-a", "lane-b"])
+    XCTAssertEqual(Set(firstStartedIds), Set(["lane-a", "lane-b"]))
+    XCTAssertEqual(firstStartedIds.count, 2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADETests/ADETests.swift` around lines 5100 - 5104, The test
currently asserts strict ordering with XCTAssertEqual(firstStartedIds,
["lane-a", "lane-b"]) which can flake due to scheduler timing; change this to an
order-insensitive assertion (e.g., compare Set(firstStartedIds) to
Set(["lane-a", "lane-b"]) or assert both expected IDs are present regardless of
order) while keeping the max active count check (firstMaxActiveCount) intact;
update the assertion that references recorder.startedIds(), firstStartedIds and
the XCTAssertEqual call accordingly.

@arul28 arul28 changed the title Creating Lane While Lane Deletes Fix concurrent lane deletes May 28, 2026
@arul28 arul28 merged commit 076b4b3 into main May 28, 2026
30 checks passed
@arul28 arul28 deleted the ade/creating-lane-while-lane-deletes-615cb70d branch May 28, 2026 16:03
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.

2 participants