Part of #359 (Epic 4: Parallel Tool Execution).
Depends on: Story 4.2b (Core parallel dispatch).
Context
cline.ask() writes to clineMessages and waits for a response. If two parallel tools both call ask() concurrently, their writes interleave and produce broken UI state — the second approval prompt would overwrite the first before it's answered. This story adds a write-through promise chain to ask() so concurrent approval requests are serialized transparently.
Developer Notes
In src/core/task/Task.ts:
- Add
private approvalQueue = pLimit(1) — p-limit is already in deps at ^6.2.0. Using pLimit(1) instead of a hand-rolled promise chain provides better error isolation — a rejected promise in a raw chain breaks the queue unless errors are explicitly caught, whereas p-limit handles this correctly.
- Wrap
ask() so concurrent calls serialize: return this.approvalQueue(() => actualAsk(...)).
- Non-approval paths (auto-approved tools that never call
ask()) are unaffected.
- Abort drain behavior: When
abortTask(true) fires while approvals are queued, pending approvals resolve with denial (not rejection) — producing valid tool_denied results. The queue settles cleanly with no unhandled rejections.
Files: src/core/task/Task.ts
Tests (extend src/core/assistant-message/__tests__/presentAssistantMessageParallel.spec.ts):
- Two parallel tools both requiring approval: second prompt never appears before first resolves. Use controlled promises for
ask() mock.
- A third tool with auto-approval runs without waiting for the approval queue.
- Abort drain: trigger
abortTask(true) while two approvals are queued → both resolve with denial → userMessageContent has valid tool_denied results for both with no unhandled rejections.
Acceptance Criteria
- No concurrent
ask() calls produce overlapping UI state.
- Sequential tool execution (flag off) is unaffected.
- Abort during queued approvals produces valid results — no unhandled rejections, no missing
tool_result entries.
Part of #359 (Epic 4: Parallel Tool Execution).
Depends on: Story 4.2b (Core parallel dispatch).
Context
cline.ask()writes toclineMessagesand waits for a response. If two parallel tools both callask()concurrently, their writes interleave and produce broken UI state — the second approval prompt would overwrite the first before it's answered. This story adds a write-through promise chain toask()so concurrent approval requests are serialized transparently.Developer Notes
In
src/core/task/Task.ts:private approvalQueue = pLimit(1)—p-limitis already in deps at^6.2.0. UsingpLimit(1)instead of a hand-rolled promise chain provides better error isolation — a rejected promise in a raw chain breaks the queue unless errors are explicitly caught, whereasp-limithandles this correctly.ask()so concurrent calls serialize:return this.approvalQueue(() => actualAsk(...)).ask()) are unaffected.abortTask(true)fires while approvals are queued, pending approvals resolve withdenial(not rejection) — producing validtool_deniedresults. The queue settles cleanly with no unhandled rejections.Files:
src/core/task/Task.tsTests (extend
src/core/assistant-message/__tests__/presentAssistantMessageParallel.spec.ts):ask()mock.abortTask(true)while two approvals are queued → both resolve with denial →userMessageContenthas validtool_deniedresults for both with no unhandled rejections.Acceptance Criteria
ask()calls produce overlapping UI state.tool_resultentries.