Skip to content

Commit 5319fe9

Browse files
authored
refactor(task): read abort and abortReason from run-state latches (#2283)
1 parent d956405 commit 5319fe9

17 files changed

Lines changed: 112 additions & 50 deletions

‎src/core/task/Task.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ import { mergeConsecutiveApiMessages } from "./mergeConsecutiveApiMessages"
144144
import { prepareApiConversationMessage } from "./apiConversationHistory"
145145
import { shouldAddUserMessageToHistory } from "./messageCounting"
146146
import { type TaskExecutionContext } from "./providerHandoff"
147-
import { initialRunState, type Ask, type RunEvent, type RunState } from "./run-state/runState"
147+
import { abort, abortReason, initialRunState, type Ask, type RunEvent, type RunState } from "./run-state/runState"
148148
import { applyRunEvent, type RunRejection } from "./run-state/runStateShadow"
149149
import {
150150
divergenceKey,
@@ -392,7 +392,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
392392

393393
providerRef: WeakRef<ClineProvider>
394394
private readonly globalStoragePath: string
395-
abort: boolean = false
396395
currentRequestAbortController?: AbortController
397396
/**
398397
* Controller for the waiter on an in-flight `ensureModelFetched()` call (see
@@ -410,7 +409,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
410409

411410
didFinishAbortingStream = false
412411
abandoned = false
413-
abortReason?: ClineApiReqCancelReason
414412
isInitialized = false
415413

416414
runState: RunState = initialRunState
@@ -3581,16 +3579,22 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
35813579
this.driveRunEvent({ tag: "completionAccepted", askTs })
35823580
}
35833581

3582+
get abort(): boolean {
3583+
return abort(this.runState)
3584+
}
3585+
3586+
get abortReason(): ClineApiReqCancelReason | undefined {
3587+
return abortReason(this.runState)
3588+
}
3589+
35843590
setAbortReason(reason: ClineApiReqCancelReason): void {
3585-
this.abortReason ??= reason
35863591
this.driveRunEvent({ tag: "reasonSet", reason })
35873592
}
35883593

35893594
requestAbort(isAbandoned: boolean): void {
35903595
if (isAbandoned) {
35913596
this.abandoned = true
35923597
}
3593-
this.abort = true
35943598
this.driveRunEvent({ tag: "abortRequested", abandoned: isAbandoned })
35953599
}
35963600

@@ -3600,7 +3604,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
36003604
}
36013605

36023606
requestDispose(): void {
3603-
this.abort = true
36043607
this.driveRunEvent({ tag: "disposeRequested" })
36053608
}
36063609

‎src/core/task/__tests__/Task.persistence.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ContextProxy } from "../../config/ContextProxy"
2121
import { providerIdentifiers } from "@roo-code/types/provider-identifiers"
2222
import { attemptCompletionTool, type AttemptCompletionCallbacks } from "../../tools/AttemptCompletionTool"
2323
import type { ToolUse } from "../../../shared/tools"
24+
import { setAbortLatch } from "../../../test-utils/run-state"
2425

2526
type TaskPersistenceAccess = {
2627
addToApiConversationHistory: (message: Anthropic.MessageParam) => Promise<void>
@@ -909,7 +910,7 @@ describe("Task persistence", () => {
909910
task: "test task",
910911
startTask: false,
911912
})
912-
task.abort = true
913+
setAbortLatch(task, true)
913914
task.userMessageContent = [{ type: "tool_result", tool_use_id: "tool-1", content: "done" }]
914915
const waitForPersistence = vi.spyOn(task, "waitForCurrentAssistantMessagePersistence")
915916

‎src/core/task/__tests__/Task.run-shadow.spec.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ describe("Task run-state write helpers", () => {
101101
task.requestAbort(false)
102102

103103
expect([task.abort, task.abandoned]).toEqual([true, false])
104-
expect(task.runState.latches).toMatchObject({ abort: true, abandoned: false })
104+
expect(task.runState.latches.abandoned).toBe(false)
105105
})
106106

107107
it("requestAbort(true) sets abort and abandoned", () => {
108108
task.requestAbort(true)
109109

110110
expect([task.abort, task.abandoned]).toEqual([true, true])
111-
expect(task.runState.latches).toMatchObject({ abort: true, abandoned: true })
111+
expect(task.runState.latches.abandoned).toBe(true)
112112
})
113113

114114
it("requestAbandon sets abandoned after an abort", () => {
@@ -124,7 +124,7 @@ describe("Task run-state write helpers", () => {
124124
task.requestDispose()
125125

126126
expect(task.abort).toBe(true)
127-
expect(task.runState.latches).toMatchObject({ abort: true, disposed: true })
127+
expect(task.runState.latches.disposed).toBe(true)
128128
})
129129

130130
it("drives the ask events", () => {
@@ -149,12 +149,15 @@ describe("Task run-state write helpers", () => {
149149
expect(task.runRejections).toEqual([{ event: { tag: "abandonRequested" }, state: initialRunState }])
150150
})
151151

152-
it("keeps the first abort reason in the field and in the kernel", () => {
152+
it("has no abort reason before one is set", () => {
153+
expect(task.abortReason).toBeUndefined()
154+
})
155+
156+
it("keeps the first abort reason", () => {
153157
task.setAbortReason("user_cancelled")
154158
task.setAbortReason("streaming_failed")
155159

156160
expect(task.abortReason).toBe("user_cancelled")
157-
expect(task.runState.latches.reason).toBe("user_cancelled")
158161
expect(task.runRejections).toEqual([])
159162
})
160163
})

‎src/core/task/__tests__/Task.spec.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ vi.mock("delay", () => ({
9999

100100
import delay from "delay"
101101
import pWaitFor from "p-wait-for"
102+
import { setAbortLatch } from "../../../test-utils/run-state"
102103

103104
vi.mock("uuid", async (importOriginal) => {
104105
const actual = await importOriginal<typeof import("uuid")>()
@@ -1071,7 +1072,7 @@ describe("Cline", () => {
10711072
const saySpy = vi.spyOn(task, "say").mockImplementation(async (...args) => {
10721073
const [type, _text, _images, partial] = args
10731074
if (type === "text" && partial === false) {
1074-
task.abort = true
1075+
setAbortLatch(task, true)
10751076
throw new Error("presenter text failed while aborting")
10761077
}
10771078
return say(...args)
@@ -4922,7 +4923,7 @@ describe("Cline", () => {
49224923
const abortTaskSpy = vi.spyOn(task, "abortTask").mockResolvedValue(undefined)
49234924

49244925
// Test Case 1: Stream failure should NOT abort task
4925-
task.abort = false
4926+
setAbortLatch(task, false)
49264927
task.abandoned = false
49274928

49284929
// Simulate the catch block behavior for stream failure
@@ -4943,7 +4944,7 @@ describe("Cline", () => {
49434944
expect(abortTaskSpy).not.toHaveBeenCalled()
49444945

49454946
// Test Case 2: User cancellation SHOULD abort task
4946-
task.abort = true
4947+
setAbortLatch(task, true)
49474948

49484949
// For user cancellation, abortTask SHOULD be called
49494950
if (task.abort) {
@@ -7344,7 +7345,7 @@ describe("Cline", () => {
73447345
await flushMicrotasks()
73457346
consoleErrorSpy.mockClear()
73467347

7347-
task.abort = true
7348+
setAbortLatch(task, true)
73487349
getTaskTestAccess(task).presentAssistantMessageSafe()
73497350
await flushMicrotasks()
73507351

@@ -7398,7 +7399,7 @@ describe("Cline", () => {
73987399
consoleErrorSpy.mockClear()
73997400

74007401
// Simulate the TOCTOU race: abort flips between throw and catch.
7401-
task.abort = true
7402+
setAbortLatch(task, true)
74027403
getTaskTestAccess(task).presentAssistantMessageSafe()
74037404
await flushMicrotasks()
74047405

‎src/core/task/__tests__/ask-abort-race.spec.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ClineMessage } from "@roo-code/types"
33
import { checkAutoApproval } from "../../auto-approval"
44
import { MessageQueueService } from "../../message-queue/MessageQueueService"
55
import { Task } from "../Task"
6+
import { setAbortLatch } from "../../../test-utils/run-state"
67

78
vi.mock("../../auto-approval", async (importOriginal) => {
89
const actual = await importOriginal<typeof import("../../auto-approval")>()
@@ -11,7 +12,7 @@ vi.mock("../../auto-approval", async (importOriginal) => {
1112

1213
function buildTask(getState: () => Promise<Record<string, unknown>>) {
1314
const task = Object.create(Task.prototype) as Task
14-
task["abort"] = false
15+
setAbortLatch(task, false)
1516
task["clineMessages"] = []
1617
task["lastMessageTs"] = undefined
1718
task["addToClineMessages"] = vi.fn(async () => {})
@@ -44,7 +45,7 @@ describe("Task.ask abort during the auto-approval await", () => {
4445
})
4546

4647
const askPromise = task.ask("tool", JSON.stringify({ tool: "readFile" }), partial)
47-
task["abort"] = true
48+
setAbortLatch(task, true)
4849
gate.release()
4950

5051
await expect(askPromise).rejects.toThrow(/aborted/)
@@ -62,7 +63,7 @@ describe("Task.ask abort during the auto-approval await", () => {
6263

6364
const askPromise = task.ask("tool", JSON.stringify({ tool: "readFile" }), false)
6465
await vi.waitFor(() => expect(checkAutoApproval).toHaveBeenCalled())
65-
task["abort"] = true
66+
setAbortLatch(task, true)
6667
gate.release()
6768

6869
await expect(askPromise).rejects.toThrow(/aborted/)
@@ -83,7 +84,7 @@ describe("Task.ask abort during the auto-approval await", () => {
8384
task["clineMessages"] = [{ ...original }]
8485

8586
const askPromise = task.ask("followup", "new", partial)
86-
task["abort"] = true
87+
setAbortLatch(task, true)
8788
gate.release()
8889

8990
await expect(askPromise).rejects.toThrow(/aborted/)
@@ -102,7 +103,7 @@ describe("Task.ask abort during the auto-approval await", () => {
102103
task.messageQueueService.addMessage("queued feedback")
103104

104105
const askPromise = task.ask("followup", "Q?", false)
105-
task["abort"] = true
106+
setAbortLatch(task, true)
106107
gate.release()
107108

108109
await expect(askPromise).rejects.toThrow(/aborted/)

‎src/core/task/__tests__/ask-allowlist-cwd.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type { ExtensionState } from "@roo-code/types"
44

55
import { Task } from "../Task"
6+
import { setAbortLatch } from "../../../test-utils/run-state"
67

78
// The allowlist patterns are resolved against a workspace root, and the path in
89
// the tool message was made relative by `getReadablePath(task.cwd, ...)`. So the
@@ -23,7 +24,7 @@ type ProviderStub = {
2324

2425
function buildTask(provider: ProviderStub, taskCwd: string) {
2526
const task = Object.create(Task.prototype) as Task
26-
task["abort"] = false
27+
setAbortLatch(task, false)
2728
task["clineMessages"] = []
2829
task["askResponse"] = undefined
2930
task["askResponseText"] = undefined

‎src/core/task/__tests__/ask-auto-deny.spec.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type ClineMessage, type ExtensionState, RooCodeEventName } from "@roo-c
55
import * as autoApprovalModule from "../../auto-approval"
66
import { createRateLimitClock } from "../RateLimitClock"
77
import { Task } from "../Task"
8+
import { setAbortLatch } from "../../../test-utils/run-state"
89

910
// The streaming-loop drive below never asserts on environment details; the
1011
// real collector reaches into the VS Code window API, which this file's
@@ -31,7 +32,7 @@ type ProviderStub = {
3132

3233
function buildTask(provider: ProviderStub, taskCwd: string) {
3334
const task = Object.create(Task.prototype) as Task
34-
task["abort"] = false
35+
setAbortLatch(task, false)
3536
task["clineMessages"] = []
3637
task["askResponse"] = undefined
3738
task["askResponseText"] = undefined
@@ -743,7 +744,7 @@ describe("Task.ask queue path cannot bypass blanket deny", () => {
743744
await vi.waitFor(() => expect(getStateCalls).toBe(2))
744745
expect(queue.hasUnclaimed()).toBe(false)
745746

746-
task["abort"] = true
747+
setAbortLatch(task, true)
747748

748749
// `ask()` must reject on abort; racing a deadline distinguishes a wrong
749750
// settle from a hang on the pending read.
@@ -830,7 +831,7 @@ describe("Task.ask queue path cannot bypass blanket deny", () => {
830831
// longer this wait runs, the less event-loop-stall slack is left before
831832
// the 2 s timer could legitimately fire while the task is still live.
832833
await vi.waitFor(() => expect(Date.now() - armedAt).toBeGreaterThanOrEqual(300), { timeout: 2_000 })
833-
task["abort"] = true
834+
setAbortLatch(task, true)
834835

835836
const outcome = await toolAsk.then(
836837
() => "resolved",
@@ -931,7 +932,7 @@ describe("Task.ask queue path cannot bypass blanket deny", () => {
931932
await vi.waitFor(() => expect(getStateCalls).toBe(2))
932933
expect(queue.hasUnclaimed()).toBe(false)
933934

934-
task["abort"] = true
935+
setAbortLatch(task, true)
935936

936937
// `ask()` must reject on abort; racing a deadline distinguishes a wrong
937938
// settle from a hang on the pending read.
@@ -993,7 +994,7 @@ describe("Task.ask queue path cannot bypass blanket deny", () => {
993994
await vi.waitFor(() => expect(getStateCalls).toBe(2))
994995
expect(queue.hasUnclaimed()).toBe(false)
995996

996-
task["abort"] = true
997+
setAbortLatch(task, true)
997998

998999
// `ask()` must reject on abort; racing a deadline distinguishes a wrong
9991000
// settle from a hang on the pending read.

‎src/core/task/__tests__/ask-clear-approval-buttons.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Task } from "../Task"
2+
import { setAbortLatch } from "../../../test-utils/run-state"
23

34
// When the backend auto-resolves an interactive ask, isAnswered:true is stamped
45
// on the ClineMessage before it is added so the webview state snapshot already
@@ -12,7 +13,7 @@ type ProviderStub = {
1213

1314
function buildTask(provider: ProviderStub | undefined) {
1415
const task = Object.create(Task.prototype) as Task
15-
;(task as any).abort = false
16+
setAbortLatch(task, false)
1617
;(task as any).clineMessages = []
1718
;(task as any).askResponse = undefined
1819
;(task as any).askResponseText = undefined

‎src/core/task/__tests__/ask-queued-message-drain.spec.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Task } from "../Task"
2+
import { setAbortLatch } from "../../../test-utils/run-state"
23

34
type QueueTaskTestAccess = {
45
say: Task["say"]
56
saveClineMessages: () => Promise<boolean>
67
addToClineMessages: () => Promise<void>
78
lastMessageTs?: number
8-
abort: boolean
99
}
1010

1111
const getQueueTaskTestAccess = (task: Task) => task as unknown as QueueTaskTestAccess
@@ -16,7 +16,7 @@ const getQueueTaskTestAccess = (task: Task) => task as unknown as QueueTaskTestA
1616
describe("Task.ask queued message drain", () => {
1717
function createTask(provider?: { getState: () => Promise<Record<string, boolean>> }) {
1818
const task = Object.create(Task.prototype) as Task
19-
;(task as any).abort = false
19+
setAbortLatch(task, false)
2020
;(task as any).clineMessages = []
2121
;(task as any).askResponse = undefined
2222
;(task as any).askResponseText = undefined
@@ -346,7 +346,7 @@ describe("Task.ask queued message drain", () => {
346346
task.messageQueueService.addMessage("Persist me later")
347347
const ask = task.ask("completion_result", "Done", false)
348348
await Promise.resolve()
349-
access.abort = true
349+
setAbortLatch(task, true)
350350
finishAddingAsk()
351351

352352
await expect(ask).rejects.toThrow("aborted")
@@ -398,7 +398,7 @@ describe("Task.ask queued message drain", () => {
398398
await vi.advanceTimersByTimeAsync(0)
399399
expect(access.saveClineMessages).toHaveBeenCalledTimes(1)
400400

401-
access.abort = true
401+
setAbortLatch(task, true)
402402
await vi.advanceTimersByTimeAsync(250)
403403

404404
await expect(persistence).resolves.toBe(false)

‎src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Task } from "../Task"
1212
import { ClineProvider } from "../../webview/ClineProvider"
1313
import { ContextProxy } from "../../config/ContextProxy"
1414
import { providerIdentifiers } from "@roo-code/types/provider-identifiers"
15+
import { setAbortLatch } from "../../../test-utils/run-state"
1516

1617
// Mock delay before any imports that might use it
1718
vi.mock("delay", () => ({
@@ -458,7 +459,7 @@ describe("flushPendingToolResultsToHistory", () => {
458459

459460
const waitForPersistence = vi.spyOn(task, "waitForCurrentAssistantMessagePersistence")
460461

461-
task.abort = true
462+
setAbortLatch(task, true)
462463

463464
await task.flushPendingToolResultsToHistory()
464465

0 commit comments

Comments
 (0)