fix: bound clear-done cleanup concurrency to avoid runtime freeze on large boards - #564
Conversation
…large boards Clearing the Done column fired stopTaskSession + cleanupTaskWorkspace for every trashed task in a single unbounded Promise.all. Because the tRPC client uses httpBatchLink, all same-tick mutations coalesce into one batched request and the server executes every procedure concurrently — on a board with 100+ done tasks that meant 100+ simultaneous session stops and git worktree deletions against the shared repo, which froze and crashed the app. Wrap the per-task cleanup chain in p-limit(4) so at most four tasks are stopped/cleaned at a time. Adds a regression test asserting the cleanup fan-out stays bounded while still covering every task.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Greptile SummaryThis PR fixes a runtime freeze when clearing the Done column on large boards by bounding the per-task cleanup fan-out with
Confidence Score: 5/5Safe to merge — the change is narrowly scoped to the clear-trash path, adds a well-tested concurrency bound, and preserves existing error-swallowing semantics. The fix is minimal and correct: pLimit(4) is instantiated fresh per invocation, wraps the full stop→cleanup chain, and lets pLimit's queue handle backpressure. The regression test instruments both cleanup functions, tracks in-flight concurrency across the full chain lifetime, and asserts both the bound and completeness. The p-limit dependency is ESM-compatible with the project's Vite + type:module setup. No files require special attention.
|
| Filename | Overview |
|---|---|
| web-ui/src/hooks/use-board-interactions.ts | Core fix: adds pLimit(4) wrapper around the stopTaskSession→cleanupTaskWorkspace chain in handleConfirmClearTrash; minimal, well-scoped change with clear explanatory comment. |
| web-ui/src/hooks/use-board-interactions.test.tsx | New regression test with 25 trashed tasks correctly tracks full-chain concurrency and asserts maxInFlight ≤ 4 and full coverage. |
| web-ui/package.json | Adds p-limit@^7.3.0 as a production dependency; compatible with the project's ESM setup. |
| web-ui/package-lock.json | Lock file adds p-limit@7.3.0 and its only transitive dependency yocto-queue@1.2.2; integrity hashes and peer requirements look correct. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User (click "Clear Done")
participant H as handleConfirmClearTrash
participant L as pLimit(4)
participant S as stopTaskSession (tRPC)
participant C as cleanupTaskWorkspace (tRPC)
participant B as httpBatchLink → Server
U->>H: confirm dialog
H->>H: setBoard / setSessions (optimistic clear)
H->>L: create pLimit(4) queue
loop for each of N tasks (max 4 at a time)
L->>S: stopTaskSession(taskId)
S->>B: batched RPC (≤4 in flight)
B-->>S: ok
S-->>L: resolved
L->>C: cleanupTaskWorkspace(taskId)
C->>B: batched RPC (≤4 in flight)
B-->>C: ok (git ops complete)
C-->>L: resolved → slot released
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as User (click "Clear Done")
participant H as handleConfirmClearTrash
participant L as pLimit(4)
participant S as stopTaskSession (tRPC)
participant C as cleanupTaskWorkspace (tRPC)
participant B as httpBatchLink → Server
U->>H: confirm dialog
H->>H: setBoard / setSessions (optimistic clear)
H->>L: create pLimit(4) queue
loop for each of N tasks (max 4 at a time)
L->>S: stopTaskSession(taskId)
S->>B: batched RPC (≤4 in flight)
B-->>S: ok
S-->>L: resolved
L->>C: cleanupTaskWorkspace(taskId)
C->>B: batched RPC (≤4 in flight)
B-->>C: ok (git ops complete)
C-->>L: resolved → slot released
end
Reviews (1): Last reviewed commit: "fix: bound clear-done cleanup concurrenc..." | Re-trigger Greptile
The problem
Clearing the Done column on a board with many tasks crashes/freezes the app. This happened on a real board with 127 done tasks: confirming "Clear done items permanently?" killed the runtime, and because the app died before the emptied board was persisted, all 127 tasks were still there on restart.
Root cause
handleConfirmClearTrash(web-ui/src/hooks/use-board-interactions.ts) fanned out cleanup for every trashed task in one unboundedPromise.all:The non-obvious part is why this is so much worse than "127 HTTP requests": the runtime tRPC client (
web-ui/src/runtime/trpc-client.ts) useshttpBatchLink, which coalesces all mutations fired in the same tick into a single batched HTTP request — and the tRPC server resolves every procedure in a batch concurrently. So browser per-origin connection limits never throttle anything. One click produced:stopTaskSessionprocedures — each going through the scoped Cline task-session service / terminal managerdeleteTaskWorktreecalls, each running git subprocesses (captureTaskPatch,git worktree remove,git worktree prune) against the same shared repo, contending on.gitlocksThis is exactly the "many subprocess spawns per task at once freezes the runtime" failure mode already documented in AGENTS.md for task-agent startup — this was the teardown-side twin of it.
The fix
Bound the fan-out with
p-limit(newweb-uidependency): at most 4 per-task cleanup chains (stop → cleanup) run at a time. SincehttpBatchLinkbatches per tick, limiting client-side concurrency directly limits server-side procedure concurrency too — at most 4 in-flight procedures instead of 127.Error semantics are unchanged: both
stopTaskSessionandcleanupTaskWorkspacealready swallow their own errors (best-effort cleanup), so nothing in the chain can reject and abandon queued tasks.Why 4, and why client-side
Promise.allfan-outs in web-ui are over small, user-selected sets (e.g. linked-task starts) — this was the only site iterating an entire column.How to test
use-board-interactions.test.tsx: builds a board with 25 trashed tasks, instrumentsstopTaskSession/cleanupTaskWorkspaceto track in-flight chains, confirms max concurrency ≤ 4 and that every task still gets stopped and cleaned. Removing the limiter makes it fail withmaxInFlight = 25.npm run typecheck,npm run lint,npm testinweb-uiall pass (498 tests).