Suppress transient runner failures in channels (#87069)

This commit is contained in:
Sarah Fortune
2026-05-26 19:30:43 -07:00
committed by GitHub
parent 698c40ef9d
commit df6ec2822f
2 changed files with 105 additions and 2 deletions
@@ -4854,6 +4854,95 @@ describe("runAgentTurnWithFallback", () => {
},
);
it.each(["group", "channel"] as const)(
"keeps classified non-transient failures visible in Discord %s chats",
async (chatType) => {
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
new Error('No API key found for provider "openai"'),
);
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const result = await runAgentTurnWithFallback(
createMinimalRunAgentTurnParams({
sessionCtx: {
Provider: "discord",
Surface: "discord",
ChatType: chatType,
GroupSubject: "agent group",
GroupChannel: "#general",
MessageSid: "msg",
} as unknown as TemplateContext,
}),
);
expect(result.kind).toBe("final");
if (result.kind === "final") {
expect(result.payload.text).not.toBe(SILENT_REPLY_TOKEN);
expect(result.payload.text).toContain('Missing API key for provider "openai"');
}
},
);
it.each(["group", "channel"] as const)(
"keeps rate-limit fallback copy out of Discord %s chats",
async (chatType) => {
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("429 rate limit exceeded"));
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const result = await runAgentTurnWithFallback(
createMinimalRunAgentTurnParams({
sessionCtx: {
Provider: "discord",
Surface: "discord",
ChatType: chatType,
GroupSubject: "agent group",
GroupChannel: "#general",
MessageSid: "msg",
} as unknown as TemplateContext,
}),
);
expect(result.kind).toBe("final");
if (result.kind === "final") {
expect(result.payload.text).toBe(SILENT_REPLY_TOKEN);
}
},
);
it("surfaces rate-limit fallback copy in Discord group chats when silentReply.group is disallow", async () => {
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("429 rate limit exceeded"));
const followupRun = createFollowupRun();
followupRun.run.config = {
agents: {
defaults: {
silentReply: { group: "disallow" },
},
},
};
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const result = await runAgentTurnWithFallback(
createMinimalRunAgentTurnParams({
followupRun,
sessionCtx: {
Provider: "discord",
Surface: "discord",
ChatType: "group",
GroupSubject: "agent group",
GroupChannel: "#general",
MessageSid: "msg",
} as unknown as TemplateContext,
}),
);
expect(result.kind).toBe("final");
if (result.kind === "final") {
expect(result.payload.text).not.toBe(SILENT_REPLY_TOKEN);
expect(result.payload.text).toContain("rate-limited");
}
});
it("uses compact generic copy for raw runner failures in normal Discord direct chats", async () => {
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"),
+16 -2
View File
@@ -643,12 +643,17 @@ function resolveExternalRunFailureTextForConversation(params: {
text: string;
sessionCtx: TemplateContext;
isGenericRunnerFailure: boolean;
suppressInNonDirect?: boolean;
cfg?: OpenClawConfig;
}): string {
if (!isNonDirectConversationContext(params.sessionCtx)) {
return params.text;
}
if (!params.isGenericRunnerFailure && !params.text.includes(AGENT_FAILED_BEFORE_REPLY_TEXT)) {
if (
!params.suppressInNonDirect &&
!params.isGenericRunnerFailure &&
!params.text.includes(AGENT_FAILED_BEFORE_REPLY_TEXT)
) {
return params.text;
}
// Match normal reply routing: default group/channel failures stay silent,
@@ -826,6 +831,7 @@ export function buildKnownAgentRunFailureReplyPayload(params: {
text: buildRateLimitCooldownMessage(params.err),
sessionCtx: params.sessionCtx,
isGenericRunnerFailure: false,
suppressInNonDirect: true,
cfg: params.cfg,
}),
});
@@ -837,6 +843,7 @@ export function buildKnownAgentRunFailureReplyPayload(params: {
text: rateLimitOrOverloadedCopy,
sessionCtx: params.sessionCtx,
isGenericRunnerFailure: false,
suppressInNonDirect: true,
cfg: params.cfg,
}),
});
@@ -2806,6 +2813,7 @@ export async function runAgentTurnWithFallback(params: {
text: fallbackText,
sessionCtx: params.sessionCtx,
isGenericRunnerFailure: externalRunFailureReply?.isGenericRunnerFailure ?? false,
suppressInNonDirect: Boolean(isRateLimit || rateLimitOrOverloadedCopy),
cfg: params.followupRun.run.config,
});
@@ -2870,7 +2878,13 @@ export async function runAgentTurnWithFallback(params: {
if (formattedErrorCandidate) {
runResult.payloads = [
markAgentRunFailureReplyPayload({
text: formattedErrorCandidate,
text: resolveExternalRunFailureTextForConversation({
text: formattedErrorCandidate,
sessionCtx: params.sessionCtx,
isGenericRunnerFailure: false,
suppressInNonDirect: true,
cfg: params.followupRun.run.config,
}),
isError: true,
}),
];