Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cf5abe6
fix(task): stage-independent saveClineMessages + finalize open partia…
easonLiangWorldedtech Oct 5, 2026
d172c95
feat(write-to-file): per-task partial stream state + cleanup primitives
easonLiangWorldedtech Oct 5, 2026
52699c6
test(write-to-file): cover partial-state cleanup primitives directly
easonLiangWorldedtech Oct 5, 2026
3bd6bbe
fix(tools): scope the write_to_file stream teardown to one task and f…
Oct 6, 2026
d8ee1fe
chore: trigger a fresh review pass at this head
Oct 7, 2026
1d4a2a6
fix(write-to-file): release stream state on every execute() exit and …
Oct 8, 2026
2f356e6
fix(write-to-file): check stream liveness after the diff-view open() …
Oct 8, 2026
ef49801
Merge branch 'main' into p1066/u4-per-task-stream-state
easonLiangWorldedtech Oct 9, 2026
9a06ac9
fix(write-to-file): release the partial stream state on every tool-ca…
Oct 9, 2026
2a8d6e4
Merge org main (036245c5e, U1 #1927) into p1066/u4-per-task-stream-state
Oct 9, 2026
80fb429
fix(write-to-file): release per-task stream state when the parameter …
easonLiangWorldedtech Oct 9, 2026
fb21709
fix(write-to-file): report rollback failures and guard every partial …
easonLiangWorldedtech Oct 9, 2026
61dd05a
fix(write-to-file): own the diff-view failure and record what the str…
easonLiangWorldedtech Oct 10, 2026
12ec5f8
style: satisfy the repository prettier gate on the files this PR changes
easonLiangWorldedtech Oct 10, 2026
8084eab
fix(write-to-file): keep rollback honest when the discard itself fails
easonLiangWorldedtech Oct 10, 2026
9e84c77
fix(abort-r1-u4): release a stream the presenter rejects
easonLiangWorldedtech Oct 10, 2026
1b387ee
fix(abort-r1-u4): keep a failed report from owning the teardown
easonLiangWorldedtech Oct 10, 2026
1374085
fix(abort-r1-u4): account for directories created before the diff view
easonLiangWorldedtech Oct 10, 2026
cfc5464
style(abort-r1-u4): reflow a chained mock the formatter splits differ…
easonLiangWorldedtech Oct 10, 2026
4cfb498
fix(abort-r1-u4): finish the teardowns the new discard path left open
easonLiangWorldedtech Oct 10, 2026
f046c92
fix(file-safety): remove adopted directories before resetting a denie…
easonLiangWorldedtech Oct 10, 2026
c02deeb
test(write-to-file): adopt directories before asserting their cleanup
Oct 10, 2026
237ee41
test(write-to-file): cover a failed discard on the rooignore-denial exit
Oct 11, 2026
4645d26
fix(write-to-file): finish the teardown when the approved save or its…
easonliang28 Oct 12, 2026
4a1894e
Merge upstream main 995167eed into the U4 unit branch
easonliang28 Oct 12, 2026
25ac09b
Stop the write teardown from touching an approved write that landed
easonliang28 Oct 12, 2026
03166ac
Record the persistence commit at the disk write, not at the save call…
easonliang28 Oct 12, 2026
02e77b9
Format the new provider test to the repository style
easonliang28 Oct 12, 2026
affdc70
Discard the streamed preview on the early exits and after a cancelled…
easonliang28 Oct 12, 2026
3ba1efb
Merge main into p1066/u4-per-task-stream-state
easonliang28 Oct 12, 2026
9d2ab75
Pass the request policy snapshot to the presenter in the rejection spec
easonliang28 Oct 12, 2026
c4ec469
Report cancelled-teardown discard failures and release before the har…
easonliang28 Oct 12, 2026
a30bedb
Release adopted directories on the parse-failure teardown too
easonliang28 Oct 12, 2026
bef984c
Pin the composite taskId.instanceId key with a same-task replacement …
easonliang28 Oct 12, 2026
8a2465d
Prove the hard-block release completes before the prompt opens
easonliang28 Oct 12, 2026
0897b24
End placeholder ownership at the delete attempt, not its success
easonliang28 Oct 12, 2026
f3c478e
chore(tools): drop Stryker suppression comment per maintainer directive
easonliang28 Oct 14, 2026
5578c75
test: clear unused bindings exposed by re-enabled unused-variable lint
easonliang28 Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/__tests__/removeClineFromStack-delegation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ClineProvider } from "../core/webview/ClineProvider"
import { TaskRegistry } from "../core/task/TaskRegistry"
import { PendingActionSettlementError, type Task } from "../core/task/Task"
import { makeProviderStub } from "./helpers/provider-stub"
import { writeToFileTool } from "../core/tools/WriteToFileTool"

type MockTask = Pick<Task, "taskId" | "instanceId"> &
Partial<Pick<Task, "parentTaskId" | "abort" | "abandoned">> & {
Expand Down Expand Up @@ -208,6 +209,46 @@ describe("ClineProvider failed history restoration cleanup", () => {
expect(task.dispose).toHaveBeenCalledOnce()
})

it("releases the tool's per-task state before a directly disposed task loses its listeners", async () => {
const key = "failed-history-task.inst-1"
const order: string[] = []
const task = {
taskId: "failed-history-task",
instanceId: "inst-1",
emit: vi.fn(),
once: vi.fn(),
off: vi.fn(),
dispose: vi.fn().mockImplementation(() => {
// Dispose removes every listener, so this is the last moment the abort
// cleanup could still have run; the entry must already be gone here.
order.push(writeToFileTool["taskPartialStreamState"].has(key) ? "state-retained" : "state-cleared")
order.push("dispose")
return Promise.resolve()
}),
} as unknown as Task
// Fixture: the state entry the tool would have created during a partial stream.
writeToFileTool["taskPartialStreamState"].set(key, {
lastSeenPartialPath: undefined,
streamFailed: false,
streamError: undefined,
task,
abortCleanup: () => {},
})
const taskEventListeners = new Map([[task, [vi.fn()]]])
const taskRegistry = new TaskRegistry()
taskRegistry.push(task)
const provider = { taskRegistry, taskEventListeners, log: vi.fn() } as unknown as ClineProvider

await privateClineProvider.cleanupFailedHistoryTask.call(
provider,
task,
new PendingActionSettlementError("settlement failed"),
)

expect(order).toEqual(["state-cleared", "dispose"])
expect(writeToFileTool["taskPartialStreamState"].size).toBe(0)
})

it("keeps the task active after an unrelated history resume failure", async () => {
const cleanupListener = vi.fn()
const task = {
Expand All @@ -233,6 +274,103 @@ describe("ClineProvider failed history restoration cleanup", () => {
expect(taskEventListeners.has(task)).toBe(true)
expect(task.dispose).not.toHaveBeenCalled()
})

it("leaves a stale task's stream state, listeners, and disposal alone when the registry no longer holds it", async () => {
// cleanupFailedHistoryTask() releases the tool's per-task state only after the
// PendingActionSettlementError check and the registry identity check. A settlement
// failure for a task the registry has already dropped must not clear the stale task's
// state, run its listeners, or dispose it: a regression that cleared anyway would tear
// down state a later resume still owns.
const cleanupListener = vi.fn()
const stale = {
taskId: "stale-history-task",
instanceId: "inst-9",
emit: vi.fn(),
once: vi.fn(),
off: vi.fn(),
dispose: vi.fn().mockResolvedValue(undefined),
}
const key = "stale-history-task.inst-9"
writeToFileTool["taskPartialStreamState"].set(key, {
lastSeenPartialPath: undefined,
streamFailed: false,
streamError: undefined,
task: stale as unknown as Task,
abortCleanup: () => {},
})
const taskEventListeners = new Map([[stale, [cleanupListener]]])
const taskRegistry = new TaskRegistry()
const provider = { taskRegistry, taskEventListeners, log: vi.fn() } as unknown as ClineProvider

try {
await privateClineProvider.cleanupFailedHistoryTask.call(
provider,
stale as unknown as Task,
new PendingActionSettlementError("settlement failed"),
)

expect(writeToFileTool["taskPartialStreamState"].has(key)).toBe(true)
expect(stale.off).not.toHaveBeenCalled()
expect(cleanupListener).not.toHaveBeenCalled()
expect(taskEventListeners.has(stale)).toBe(true)
expect(stale.emit).not.toHaveBeenCalled()
expect(stale.dispose).not.toHaveBeenCalled()
} finally {
writeToFileTool["taskPartialStreamState"].clear()
}
})

it("leaves a stale task alone when the registry holds a replacement task under the same id", async () => {
// Identity, not presence: a re-created task owns the id now. Clearing the stale object
// would release a stream entry the failed resume no longer owns while leaving the
// replacement's own bookkeeping untouched.
const cleanupListener = vi.fn()
const stale = {
taskId: "replaced-history-task",
instanceId: "inst-9",
emit: vi.fn(),
once: vi.fn(),
off: vi.fn(),
dispose: vi.fn().mockResolvedValue(undefined),
}
const replacement = {
taskId: "replaced-history-task",
instanceId: "inst-10",
emit: vi.fn(),
dispose: vi.fn().mockResolvedValue(undefined),
}
const key = "replaced-history-task.inst-9"
writeToFileTool["taskPartialStreamState"].set(key, {
lastSeenPartialPath: undefined,
streamFailed: false,
streamError: undefined,
task: stale as unknown as Task,
abortCleanup: () => {},
})
const taskEventListeners = new Map([[stale, [cleanupListener]]])
const taskRegistry = new TaskRegistry()
taskRegistry.push(replacement as unknown as Task)
const provider = { taskRegistry, taskEventListeners, log: vi.fn() } as unknown as ClineProvider

try {
await privateClineProvider.cleanupFailedHistoryTask.call(
provider,
stale as unknown as Task,
new PendingActionSettlementError("settlement failed"),
)

expect(writeToFileTool["taskPartialStreamState"].has(key)).toBe(true)
expect(stale.off).not.toHaveBeenCalled()
expect(cleanupListener).not.toHaveBeenCalled()
expect(taskEventListeners.has(stale)).toBe(true)
expect(stale.emit).not.toHaveBeenCalled()
expect(stale.dispose).not.toHaveBeenCalled()
// The replacement keeps its registry slot: the stale cleanup stopped at the guard.
expect(taskRegistry.getById("replaced-history-task")).toBe(replacement)
} finally {
writeToFileTool["taskPartialStreamState"].clear()
}
})
})

describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", () => {
Expand Down
Loading
Loading