Skip to content

Commit 7ffba2f

Browse files
committed
test(agents): cover idle timeout fallback during tools
1 parent 928a570 commit 7ffba2f

3 files changed

Lines changed: 58 additions & 15 deletions

File tree

src/agents/pi-embedded-runner/run/assistant-failover.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export async function handleAssistantFailover(params: {
9898

9999
if (decision.action === "rotate_profile") {
100100
const failedProfileId = params.lastProfileId;
101-
const failureReason =
102-
params.timedOut || params.idleTimedOut ? "timeout" : params.assistantProfileFailureReason;
101+
const timeoutFailure = params.timedOut || params.idleTimedOut;
102+
const failureReason = timeoutFailure ? "timeout" : params.assistantProfileFailureReason;
103103
const markFailedProfile = async () => {
104104
if (!failedProfileId || !failureReason || failureReason === "timeout") {
105105
return;
@@ -155,11 +155,9 @@ export async function handleAssistantFailover(params: {
155155

156156
const rotated = await params.advanceAuthProfile();
157157
const markFailedProfilePromise = markFailedProfile();
158-
if (params.timedOut && !params.isProbeSession && failedProfileId) {
159-
params.warn(`Profile ${failedProfileId} timed out. Trying next account...`);
160-
}
161-
if (params.idleTimedOut && !params.isProbeSession && failedProfileId) {
162-
params.warn(`Profile ${failedProfileId} idle timeout (model silent). Trying next account...`);
158+
if (timeoutFailure && !params.isProbeSession && failedProfileId) {
159+
const timeoutLabel = params.idleTimedOut ? "idle timeout (model silent)" : "timed out";
160+
params.warn(`Profile ${failedProfileId} ${timeoutLabel}. Trying next account...`);
163161
}
164162
if (params.cloudCodeAssistFormatError && failedProfileId) {
165163
params.warn(
@@ -289,6 +287,7 @@ function resolveAssistantFailoverErrorMessage(params: {
289287
billingFailure: boolean;
290288
authFailure: boolean;
291289
}): string {
290+
const timeoutFailure = params.timedOut || params.idleTimedOut;
292291
return (
293292
(params.lastAssistant
294293
? formatAssistantErrorText(params.lastAssistant, {
@@ -299,7 +298,7 @@ function resolveAssistantFailoverErrorMessage(params: {
299298
})
300299
: undefined) ||
301300
params.lastAssistant?.errorMessage?.trim() ||
302-
(params.timedOut || params.idleTimedOut
301+
(timeoutFailure
303302
? "LLM request timed out."
304303
: params.rateLimitFailure
305304
? "LLM request rate limited."

src/agents/pi-embedded-runner/run/failover-policy.test.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,48 @@ describe("resolveRunFailoverDecision", () => {
279279
});
280280
});
281281

282+
it("treats idle watchdog timeouts during tool execution as model silence", () => {
283+
expect(
284+
resolveRunFailoverDecision({
285+
stage: "assistant",
286+
aborted: true,
287+
externalAbort: false,
288+
fallbackConfigured: true,
289+
failoverFailure: false,
290+
failoverReason: null,
291+
timedOut: true,
292+
idleTimedOut: true,
293+
timedOutDuringCompaction: false,
294+
timedOutDuringToolExecution: true,
295+
profileRotated: false,
296+
}),
297+
).toEqual({
298+
action: "rotate_profile",
299+
reason: null,
300+
});
301+
});
302+
303+
it("falls back after idle watchdog timeout during tool execution exhausts profile rotation", () => {
304+
expect(
305+
resolveRunFailoverDecision({
306+
stage: "assistant",
307+
aborted: true,
308+
externalAbort: false,
309+
fallbackConfigured: true,
310+
failoverFailure: false,
311+
failoverReason: null,
312+
timedOut: true,
313+
idleTimedOut: true,
314+
timedOutDuringCompaction: false,
315+
timedOutDuringToolExecution: true,
316+
profileRotated: true,
317+
}),
318+
).toEqual({
319+
action: "fallback_model",
320+
reason: "timeout",
321+
});
322+
});
323+
282324
it("does not rotate or fallback assistant timeouts after an external abort", () => {
283325
expect(
284326
resolveRunFailoverDecision({
@@ -301,9 +343,6 @@ describe("resolveRunFailoverDecision", () => {
301343
});
302344

303345
it("rotates profile on LLM idle timeout before falling back", () => {
304-
// idleTimedOut = model produced no tokens; no provider API error was classified.
305-
// Before this fix, failoverReason=null + timedOut=false → shouldRotateAssistant=false
306-
// → continue_normal, causing a silent agent freeze.
307346
expect(
308347
resolveRunFailoverDecision({
309348
stage: "assistant",

src/agents/pi-embedded-runner/run/failover-policy.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,20 @@ function shouldRotatePrompt(params: PromptDecisionParams): boolean {
9393
);
9494
}
9595

96+
function isAssistantTimeoutFailure(params: AssistantDecisionParams): boolean {
97+
return (
98+
params.idleTimedOut ||
99+
(params.timedOut && !params.timedOutDuringCompaction && !params.timedOutDuringToolExecution)
100+
);
101+
}
102+
96103
function shouldRotateAssistant(params: AssistantDecisionParams): boolean {
97104
if (isTerminalFormatFailure(params)) {
98105
return false;
99106
}
100107
return (
101108
(!params.aborted && (params.failoverFailure || params.failoverReason !== null)) ||
102-
(params.timedOut && !params.timedOutDuringCompaction && !params.timedOutDuringToolExecution) ||
103-
params.idleTimedOut
109+
isAssistantTimeoutFailure(params)
104110
);
105111
}
106112

@@ -180,8 +186,7 @@ export function resolveRunFailoverDecision(params: RunFailoverDecisionParams): R
180186
if (assistantShouldRotate && params.fallbackConfigured) {
181187
return {
182188
action: "fallback_model",
183-
reason:
184-
params.timedOut || params.idleTimedOut ? "timeout" : (params.failoverReason ?? "unknown"),
189+
reason: isAssistantTimeoutFailure(params) ? "timeout" : (params.failoverReason ?? "unknown"),
185190
};
186191
}
187192
if (!assistantShouldRotate) {

0 commit comments

Comments
 (0)