Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions src/tui/turn-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,49 @@ describe("turnStateFromEvent", () => {
expect(bothDone.activeToolCalls).toHaveLength(0);
});

test("tool.done only sets awaitingResponse once every parallel call has finished", () => {
// Regression for CL-5661: with a fan-out of two outstanding calls, the
// first tool.done must not claim the turn is idle while the second call
// is still running — that falsely tells consumers (stall watchdog,
// status chrome) the model is the only thing left to wait on.
const running = fold([
{ type: "inference.start" },
{ type: "tool.start", data: { call: { id: "call_1", name: "grep" } } },
{ type: "tool.start", data: { call: { id: "call_2", name: "bash" } } },
]);
expect(running.activeToolCalls).toHaveLength(2);
expect(running.awaitingResponse).toBe(false);

const oneDone = turnStateFromEvent(
running,
{ type: "tool.done", data: { result: { callId: "call_1" } } },
200,
);
expect(oneDone.activeToolCalls).toHaveLength(1);
expect(oneDone.awaitingResponse).toBe(false);

const bothDone = turnStateFromEvent(
oneDone,
{ type: "tool.done", data: { result: { callId: "call_2" } } },
201,
);
expect(bothDone.activeToolCalls).toHaveLength(0);
expect(bothDone.awaitingResponse).toBe(true);
});

test("a lone tool.done still sets awaitingResponse", () => {
const running = fold([
{ type: "inference.start" },
{ type: "tool.start", data: { call: { id: "call_1", name: "bash" } } },
]);
const done = turnStateFromEvent(
running,
{ type: "tool.done", data: { result: { callId: "call_1" } } },
200,
);
expect(done.awaitingResponse).toBe(true);
});

test("a second call to the same tool name does not inherit a finished call's id", () => {
const firstDone = fold([
{ type: "inference.start" },
Expand Down
10 changes: 6 additions & 4 deletions src/tui/turn-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,22 +591,24 @@ export function turnStateFromEvent(
return {
...state,
...tracking,
awaitingResponse: true,
awaitingResponse: tracking.activeToolCalls.length === 0,
streamingType: null,
currentToolName: null,
lastActivityAt: nowMs,
};
}

case "tool_result":
case "tool_result": {
const activeToolCalls = withoutActiveCall(state.activeToolCalls, event.name ?? "tool");
return {
...state,
awaitingResponse: true,
awaitingResponse: activeToolCalls.length === 0,
streamingType: null,
currentToolName: null,
lastActivityAt: nowMs,
activeToolCalls: withoutActiveCall(state.activeToolCalls, event.name ?? "tool"),
activeToolCalls,
};
}

/**
* A cycle with no active tool calls left is also a turn's real
Expand Down
Loading