Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline de8a085e9d test(core): reproduce duplicate tool call IDs 2026-08-07 22:00:40 +00:00
2 changed files with 56 additions and 56 deletions
@@ -498,4 +498,60 @@ Recent work
},
])
})
test("does not lower duplicate tool call IDs from interrupted history", () => {
const messages = toLLMMessages(
[
SessionMessage.Assistant.make({
id: id("duplicate-tool-call"),
type: "assistant",
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantTool.make({
type: "tool",
id: "call_1",
name: "read",
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { path: "README.md" },
content: [{ type: "text", text: "done" }],
structured: {},
}),
time: { created, completed: created },
}),
SessionMessage.AssistantTool.make({
type: "tool",
id: "call_1",
name: "unknown",
state: SessionMessage.ToolStateError.make({
status: "error",
input: {},
content: [],
structured: {},
error: { type: "unknown", message: "Tool execution interrupted" },
}),
time: { created, completed: created },
}),
],
time: { created, completed: created },
}),
],
model,
)
const calls = messages.flatMap((message) =>
message.content.filter((part) => part.type === "tool-call" && part.id === "call_1"),
)
expect(calls).toEqual([
{
type: "tool-call",
id: "call_1",
name: "read",
input: { path: "README.md" },
providerExecuted: undefined,
providerMetadata: undefined,
},
])
})
})
-56
View File
@@ -1769,62 +1769,6 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("replays interrupted provider-local tool call IDs uniquely", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo twice" }), resume: false })
requests.length = 0
executions.length = 0
const firstGate = yield* Deferred.make<void>()
const secondGate = yield* Deferred.make<void>()
toolExecutionGate = firstGate
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "tool_0", name: "echo", input: { text: "first" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "tool_0", name: "echo", input: { text: "second" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
]
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (executions.length < 1) yield* Effect.yieldNow
toolExecutionGate = secondGate
yield* Deferred.succeed(firstGate, undefined)
while (executions.length < 2) yield* Effect.yieldNow
yield* session.interrupt(sessionID)
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
toolExecutionGate = undefined
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Echo twice" },
{ type: "assistant", content: [{ type: "tool", id: "tool_0", state: { status: "completed" } }] },
{ type: "assistant", content: [{ type: "tool", id: "tool_0", state: { status: "error" } }] },
])
requests.length = 0
responses = undefined
response = []
yield* session.resume(sessionID)
const callIDs = requests[0]!.messages.flatMap((message) =>
message.role === "assistant"
? message.content.filter((part) => part.type === "tool-call").map((part) => part.id)
: [],
)
expect(callIDs).toHaveLength(2)
expect(new Set(callIDs).size).toBe(callIDs.length)
}),
)
it.effect("joins concurrent resume calls into one active provider run", () =>
Effect.gen(function* () {
yield* setup