Skip to content

Commit b4d593d

Browse files
PierrunoYTclaude
andcommitted
fix: refuse orphan tool_result during delegation rollback
- Abort rollback before saving API history or restoring the parent when the failed pending action has no matching assistant tool_use. A standalone tool_result is invalid history that native tool-call providers reject; the parent stays available for an explicit retry. - Assert the specific error for each invalidate() failure scenario (EISDIR, SyntaxError, invalid record) instead of any rejection. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent b9c2a3c commit b4d593d

3 files changed

Lines changed: 22 additions & 5 deletions

File tree

‎src/__tests__/ClineProvider.delegation.spec.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
762762
expect(durableParent.awaitingChildId).toBe("child-1")
763763
})
764764

765-
it.each(["legacy", "pending", "flushed", "resolved", "read-failure", "write-failure"] as const)(
765+
it.each(["legacy", "pending", "flushed", "resolved", "missing-tool-use", "read-failure", "write-failure"] as const)(
766766
"rolls back without automatic action replay after a metadata failure (%s)",
767767
async (scenario) => {
768768
const persistError = new Error("parent metadata persist failed")
@@ -803,6 +803,10 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
803803
],
804804
})
805805
}
806+
if (scenario === "missing-tool-use") {
807+
// No owning tool_use: a standalone tool_result would be rejected by native tool-call providers.
808+
durableMessages = []
809+
}
806810
if (scenario === "resolved") {
807811
durableMessages.push({
808812
role: "user",
@@ -893,7 +897,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
893897
expect(removeClineFromStack).toHaveBeenNthCalledWith(1)
894898
expect(removeClineFromStack).toHaveBeenNthCalledWith(2)
895899
expect(deleteTaskWithId).toHaveBeenCalledWith("child-1", false)
896-
if (scenario === "read-failure" || scenario === "write-failure") {
900+
if (scenario === "read-failure" || scenario === "write-failure" || scenario === "missing-tool-use") {
897901
expect(createTaskWithHistoryItem).not.toHaveBeenCalled()
898902
} else {
899903
expect(createTaskWithHistoryItem).toHaveBeenCalledExactlyOnceWith(historyItem)

‎src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ describe("TaskHistoryStore", () => {
127127
await expect(store.invalidate("owner")).resolves.toBeUndefined()
128128
expect(store.get("owner")).toBeUndefined()
129129
} else {
130-
await expect(store.invalidate("owner")).rejects.toThrow()
130+
await expect(store.invalidate("owner")).rejects.toMatchObject(
131+
scenario === "read-error"
132+
? { code: "EISDIR" }
133+
: scenario === "malformed"
134+
? { name: "SyntaxError" }
135+
: { message: expect.stringContaining("Invalid task history record") },
136+
)
131137
expect(store.get("owner")).toEqual(owner)
132138
}
133139
})

‎src/core/webview/ClineProvider.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4049,7 +4049,14 @@ export class ClineProvider
40494049
(block) => block.type === "tool_use" && block.id === pendingActionId,
40504050
),
40514051
)
4052-
const nextMessage = toolUseIndex === -1 ? undefined : messages[toolUseIndex + 1]
4052+
// A standalone tool_result with no owning tool_use is invalid API history that
4053+
// native tool-call providers reject; leave the parent for an explicit retry instead.
4054+
if (toolUseIndex === -1) {
4055+
throw new Error(
4056+
`[delegateParentAndOpenChild] Cannot resolve pending action ${pendingActionId}: no matching tool_use exists`,
4057+
)
4058+
}
4059+
const nextMessage = messages[toolUseIndex + 1]
40534060
if (nextMessage?.role === "user") {
40544061
const content =
40554062
typeof nextMessage.content === "string"
@@ -4059,7 +4066,7 @@ export class ClineProvider
40594066
content.splice(firstNonTool === -1 ? content.length : firstNonTool, 0, result)
40604067
messages[toolUseIndex + 1] = { ...nextMessage, content }
40614068
} else {
4062-
messages.splice(toolUseIndex === -1 ? messages.length : toolUseIndex + 1, 0, {
4069+
messages.splice(toolUseIndex + 1, 0, {
40634070
role: "user",
40644071
ts: Date.now(),
40654072
content: [result],

0 commit comments

Comments
 (0)