fix(replies): keep approval-unavailable notices durable (#121179)

Punchcard-Session: clear-lantern-summit-v2
This commit is contained in:
Leon-SK668
2026-08-13 13:23:58 +08:00
committed by GitHub
parent 568c07191a
commit 7d6dfa1939
7 changed files with 72 additions and 8 deletions
@@ -2671,6 +2671,15 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
},
},
},
{
label: "unavailable exec approvals",
payload: {
text: "Exec approval is unavailable.",
channelData: {
execApprovalUnavailable: { reason: "no-approval-route" },
},
},
},
{
label: "ask-user prompts",
payload: {
@@ -51,6 +51,10 @@ export function hasExecApprovalPayload(payload: ReplyPayload): boolean {
return isRecord(payload.channelData?.execApproval);
}
export function hasExecApprovalUnavailablePayload(payload: ReplyPayload): boolean {
return isRecord(payload.channelData?.execApprovalUnavailable);
}
export function hasAskUserPayload(payload: ReplyPayload): boolean {
return isRecord(payload.channelData?.askUser);
}
@@ -59,6 +63,7 @@ export function requiresDurableToolResultDelivery(payload: ReplyPayload): boolea
return (
resolveSendableOutboundReplyParts(payload).hasMedia ||
hasExecApprovalPayload(payload) ||
hasExecApprovalUnavailablePayload(payload) ||
hasAskUserPayload(payload)
);
}
@@ -13,7 +13,11 @@ import { normalizeMessageChannel } from "../../utils/message-channel.js";
import type { GetReplyOptions } from "../get-reply-options.types.js";
import type { ReplyPayload } from "../reply-payload.js";
import type { ChooseDispatchRouteReadyState } from "./dispatch-from-config.choose-route.js";
import { hasAskUserPayload, hasExecApprovalPayload } from "./dispatch-from-config.payloads.js";
import {
hasAskUserPayload,
hasExecApprovalPayload,
hasExecApprovalUnavailablePayload,
} from "./dispatch-from-config.payloads.js";
import { extendPreparedDispatchState } from "./dispatch-from-config.phase-state.js";
import { loadGetReplyFromConfigRuntime } from "./dispatch-from-config.runtime-loaders.js";
import { withFullRuntimeReplyConfig } from "./get-reply-fast-path.js";
@@ -121,7 +125,7 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt
if (shouldSendToolSummaries()) {
return payload;
}
if (hasExecApprovalPayload(payload)) {
if (hasExecApprovalPayload(payload) || hasExecApprovalUnavailablePayload(payload)) {
return payload;
}
if (hasAskUserPayload(payload)) {
@@ -427,6 +427,31 @@ describe("dispatchReplyFromConfig", () => {
).toBe(true);
});
it("delivers approval-unavailable notices when verbose tool progress is disabled", async () => {
setNoAbort();
const payload = {
text: "Exec approval is unavailable.",
channelData: {
execApprovalUnavailable: { reason: "no-approval-route" },
},
} satisfies ReplyPayload;
const dispatcher = createDispatcher();
const ctx = buildTestCtx({ Provider: "telegram", ChatType: "direct" });
const replyResolver = async (
_ctx: MsgContext,
opts?: GetReplyOptions,
_cfg?: OpenClawConfig,
) => {
await requireToolResultHandler(opts?.onToolResult)(payload);
return undefined;
};
await dispatchReplyFromConfig({ ctx, cfg: emptyConfig, dispatcher, replyResolver });
expect(dispatcher.sendToolResult).toHaveBeenCalledWith(payload);
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
});
it("drops ask_user prompts that terminalize before dispatcher delivery", async () => {
setNoAbort();
askUserMocks.isAskUserPromptPending.mockResolvedValue(false);
@@ -329,6 +329,15 @@ describe("executeFollowupTurn", () => {
},
},
},
{
label: "unavailable exec approvals",
payload: {
text: "Exec approval is unavailable.",
channelData: {
execApprovalUnavailable: { reason: "no-approval-route" },
},
},
},
{
label: "ask-user prompts",
payload: {
+11 -6
View File
@@ -688,18 +688,23 @@ describe("exec approval reply helpers", () => {
}),
).toEqual({
text: "Careful.\n\nApproval required. I sent approval DMs to the approvers for this account.",
channelData: {
execApprovalUnavailable: {
reason: "no-approval-route",
},
},
});
});
it.each(unavailableReasonCases)(
"builds unavailable payload for reason $reason",
({ reason, channelLabel, expected }) => {
expect(
buildExecApprovalUnavailableReplyPayload({
reason,
channelLabel,
}).text,
).toContain(expected);
const payload = buildExecApprovalUnavailableReplyPayload({
reason,
channelLabel,
});
expect(payload.text).toContain(expected);
expect(payload.channelData).toEqual({ execApprovalUnavailable: { reason } });
},
);
});
+7
View File
@@ -475,6 +475,11 @@ export function buildExecApprovalUnavailableReplyPayload(
params: ExecApprovalUnavailableReplyParams,
): ReplyPayload {
const lines: string[] = [];
const channelData = {
execApprovalUnavailable: {
reason: params.reason,
},
};
const warningText = params.warningText?.trim();
if (warningText) {
lines.push(warningText);
@@ -484,6 +489,7 @@ export function buildExecApprovalUnavailableReplyPayload(
lines.push(getExecApprovalApproverDmNoticeText());
return {
text: lines.join("\n\n"),
channelData,
};
}
@@ -535,5 +541,6 @@ export function buildExecApprovalUnavailableReplyPayload(
return {
text: lines.join("\n\n"),
channelData,
};
}