Skip to content

Commit 2b91198

Browse files
committed
Stop the stall watchdog from aborting healthy parallel tool fan-outs
awaitingResponse flips true the instant any one tool call in a parallel fan-out finishes, even while sibling calls are still running. The stall check treated that flag alone as proof of silence, so a multi-agent run with real work in flight got a false stall notice at 90s and a hard abort at 900s, killing every still-running sub-agent. Outstanding tool calls now suppress both the notice and the abort.
1 parent 2b6b0ec commit 2b91198

3 files changed

Lines changed: 24 additions & 1 deletion

File tree

src/tui-opentui/runtime-bridge.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,7 @@ export function attachSessionBridge(
951951
stallTimeoutMs,
952952
isProcessing: bag.turn.isProcessing,
953953
streamingType: bag.turn.streamingType,
954+
activeToolCalls: bag.turn.activeToolCalls,
954955
}
955956

956957
if (shouldAbortForStall(stallArgs)) {

src/tui-opentui/stall-watchdog.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,19 @@ describe("shouldAbortForStall", () => {
2020
stallTimeoutMs: STALL_TIMEOUT_MS,
2121
isProcessing: true,
2222
streamingType: null,
23+
activeToolCalls: [],
2324
}
2425

26+
test("a parallel fan-out with sibling tools still running is not a stall", () => {
27+
const args = {
28+
...base,
29+
activeToolCalls: ["call-2"],
30+
lastActivityAt: 0,
31+
nowMs: 20 * 60_000,
32+
}
33+
expect(shouldAbortForStall(args)).toBe(false)
34+
})
35+
2536
test("aborts an awaiting run past the timeout", () => {
2637
expect(shouldAbortForStall(base)).toBe(true)
2738
})
@@ -183,8 +194,15 @@ describe("shouldNoticeStall", () => {
183194
isProcessing: true,
184195
streamingType: null,
185196
repeating: false,
197+
activeToolCalls: [],
186198
}
187199

200+
test("a parallel fan-out with sibling tools still running does not notice", () => {
201+
expect(
202+
shouldNoticeStall({ ...base, activeToolCalls: ["call-2"] }),
203+
).toBe(false)
204+
})
205+
188206
test("stays quiet while repeating, even if also silent by the clock", () => {
189207
expect(shouldNoticeStall({ ...base, repeating: true })).toBe(false)
190208
})

src/tui-opentui/stall-watchdog.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type ShouldAbortForStallArgs = {
1919
readonly stallTimeoutMs: number
2020
readonly isProcessing: boolean
2121
readonly streamingType: "text" | "thinking" | "tool" | null
22+
readonly activeToolCalls: readonly string[]
2223
}
2324

2425
// The captured incident looped two sentences with no line break between them
@@ -116,7 +117,10 @@ function silentPastThreshold(
116117
): boolean {
117118
if (args.status !== "running") return false
118119
if (args.nowMs - args.lastActivityAt < thresholdMs) return false
119-
if (args.awaitingResponse) return true
120+
// A parallel fan-out flips `awaitingResponse` true the moment any one
121+
// sub-agent's tool call finishes, even while siblings are still running.
122+
// Outstanding calls mean the run is not silent, regardless of that flag.
123+
if (args.awaitingResponse && args.activeToolCalls.length === 0) return true
120124
// Mid-stream hang: model stream stalled after first token. Long in-flight
121125
// tool runs do not emit parent stream events; do not abort those.
122126
return (

0 commit comments

Comments
 (0)