fix(exec): deliver approval followups directly to chat

This commit is contained in:
Vincent Koc
2026-04-01 05:21:00 +09:00
parent 58ee76fc84
commit fc169215d7
2 changed files with 86 additions and 20 deletions

View File

@@ -4,13 +4,19 @@ vi.mock("./tools/gateway.js", () => ({
callGatewayTool: vi.fn(async () => ({ ok: true })),
}));
vi.mock("../infra/outbound/message.js", () => ({
sendMessage: vi.fn(async () => ({ ok: true })),
}));
let callGatewayTool: typeof import("./tools/gateway.js").callGatewayTool;
let sendMessage: typeof import("../infra/outbound/message.js").sendMessage;
let buildExecApprovalFollowupPrompt: typeof import("./bash-tools.exec-approval-followup.js").buildExecApprovalFollowupPrompt;
let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup.js").sendExecApprovalFollowup;
beforeEach(async () => {
vi.resetModules();
({ callGatewayTool } = await import("./tools/gateway.js"));
({ sendMessage } = await import("../infra/outbound/message.js"));
({ buildExecApprovalFollowupPrompt, sendExecApprovalFollowup } =
await import("./bash-tools.exec-approval-followup.js"));
});
@@ -48,32 +54,65 @@ describe("exec approval followup", () => {
}),
{ expectFinal: true },
);
expect(sendMessage).not.toHaveBeenCalled();
});
it("uses external delivery when a deliverable route is available", async () => {
await sendExecApprovalFollowup({
approvalId: "req-2",
it.each([
{
channel: "slack",
sessionKey: "agent:main:slack:channel:C123",
to: "channel:C123",
accountId: "default",
threadId: "1712419200.1234",
},
{
channel: "discord",
sessionKey: "agent:main:discord:channel:123",
turnSourceChannel: "discord",
turnSourceTo: "123",
turnSourceAccountId: "default",
turnSourceThreadId: "456",
resultText: "Exec completed: echo ok",
to: "123",
accountId: "default",
threadId: "456",
},
{
channel: "telegram",
sessionKey: "agent:main:telegram:-100123",
to: "-100123",
accountId: "default",
threadId: "789",
},
])("uses direct external delivery for $channel followups", async (target) => {
await sendExecApprovalFollowup({
approvalId: `req-${target.channel}`,
sessionKey: target.sessionKey,
turnSourceChannel: target.channel,
turnSourceTo: target.to,
turnSourceAccountId: target.accountId,
turnSourceThreadId: target.threadId,
resultText: "slack exec approval smoke",
});
expect(callGatewayTool).toHaveBeenCalledWith(
"agent",
expect.any(Object),
expect(sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "agent:main:discord:channel:123",
deliver: true,
bestEffortDeliver: true,
channel: "discord",
to: "123",
accountId: "default",
threadId: "456",
channel: target.channel,
to: target.to,
accountId: target.accountId,
threadId: target.threadId,
content: "slack exec approval smoke",
mirror: expect.objectContaining({
sessionKey: target.sessionKey,
idempotencyKey: `exec-approval-followup:req-${target.channel}`,
}),
}),
{ expectFinal: true },
);
expect(callGatewayTool).not.toHaveBeenCalled();
});
it("throws when neither a session nor a deliverable route is available", async () => {
await expect(
sendExecApprovalFollowup({
approvalId: "req-missing",
turnSourceChannel: "slack",
resultText: "Exec completed: echo ok",
}),
).rejects.toThrow("Session key or deliverable origin route is required");
});
});

View File

@@ -1,4 +1,6 @@
import { resolveExternalBestEffortDeliveryTarget } from "../infra/outbound/best-effort-delivery.js";
import { sendMessage } from "../infra/outbound/message.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { isGatewayMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
import { callGatewayTool } from "./tools/gateway.js";
@@ -51,7 +53,7 @@ export async function sendExecApprovalFollowup(
): Promise<boolean> {
const sessionKey = params.sessionKey?.trim();
const resultText = params.resultText.trim();
if (!sessionKey || !resultText) {
if (!resultText) {
return false;
}
@@ -67,6 +69,31 @@ export async function sendExecApprovalFollowup(
? normalizedTurnSourceChannel
: undefined;
if (deliveryTarget.deliver) {
const requesterAgentId = sessionKey ? parseAgentSessionKey(sessionKey)?.agentId : undefined;
await sendMessage({
channel: deliveryTarget.channel,
to: deliveryTarget.to ?? "",
accountId: deliveryTarget.accountId,
threadId: deliveryTarget.threadId,
content: resultText,
agentId: requesterAgentId,
idempotencyKey: `exec-approval-followup:${params.approvalId}`,
mirror: sessionKey
? {
sessionKey,
agentId: requesterAgentId,
idempotencyKey: `exec-approval-followup:${params.approvalId}`,
}
: undefined,
});
return true;
}
if (!sessionKey) {
throw new Error("Session key or deliverable origin route is required");
}
await callGatewayTool(
"agent",
{ timeoutMs: 60_000 },