Merge remote-tracking branch 'origin/main' into release/2026.4.25

This commit is contained in:
Peter Steinberger
2026-04-26 14:17:08 +01:00
2 changed files with 143 additions and 17 deletions

View File

@@ -150,6 +150,107 @@ const replyMediaPathMocks = vi.hoisted(() => ({
const runtimePluginMocks = vi.hoisted(() => ({
ensureRuntimePluginsLoaded: vi.fn(),
}));
const conversationBindingMocks = vi.hoisted(() => {
type BindingMsgContext = {
OriginatingChannel?: string | null;
Surface?: string | null;
Provider?: string | null;
AccountId?: string | null;
MessageThreadId?: string | number | null;
ThreadParentId?: string | null;
SenderId?: string | null;
SessionKey?: string | null;
ParentSessionKey?: string | null;
OriginatingTo?: string | null;
To?: string | null;
From?: string | null;
NativeChannelId?: string | null;
};
type BindingConfig = {
channels?: Record<string, { defaultAccount?: string | null } | undefined>;
};
const normalizeText = (value: string | number | null | undefined) =>
typeof value === "number" ? `${value}` : (value ?? "").trim();
const normalizeChannel = (value: string | null | undefined) => normalizeText(value).toLowerCase();
const resolveChannel = (ctx: BindingMsgContext, commandChannel?: string | null) =>
normalizeChannel(ctx.OriginatingChannel ?? commandChannel ?? ctx.Surface ?? ctx.Provider);
const resolveAccountId = (ctx: BindingMsgContext, cfg: BindingConfig, channel: string) =>
normalizeText(ctx.AccountId) ||
normalizeText(cfg.channels?.[channel]?.defaultAccount) ||
"default";
const resolveTarget = (channel: string, value: string | null | undefined) => {
const target = normalizeText(value);
if (!target) {
return undefined;
}
const channelPrefix = `${channel}:`;
return target.toLowerCase().startsWith(channelPrefix)
? target.slice(channelPrefix.length)
: target;
};
const resolveThreadId = (ctx: BindingMsgContext) =>
normalizeText(ctx.MessageThreadId) || undefined;
const resolveConversationBindingContextFromMessage = vi.fn(
(params: { cfg: BindingConfig; ctx: BindingMsgContext }) => {
const channel = resolveChannel(params.ctx);
if (!channel) {
return null;
}
const threadId = resolveThreadId(params.ctx);
const baseConversationId =
resolveTarget(channel, params.ctx.OriginatingTo) ?? resolveTarget(channel, params.ctx.To);
const conversationId = threadId ?? baseConversationId;
if (!conversationId) {
return null;
}
const parentConversationId =
threadId && baseConversationId && baseConversationId !== threadId
? baseConversationId
: resolveTarget(channel, params.ctx.ThreadParentId);
return {
channel,
accountId: resolveAccountId(params.ctx, params.cfg, channel),
conversationId,
...(parentConversationId ? { parentConversationId } : {}),
...(threadId ? { threadId } : {}),
};
},
);
return {
resolveConversationBindingAccountIdFromMessage: (params: {
ctx: BindingMsgContext;
cfg: BindingConfig;
commandChannel?: string | null;
}) =>
resolveAccountId(params.ctx, params.cfg, resolveChannel(params.ctx, params.commandChannel)),
resolveConversationBindingChannelFromMessage: (
ctx: BindingMsgContext,
commandChannel?: string | null,
) => resolveChannel(ctx, commandChannel),
resolveConversationBindingContextFromAcpCommand: (params: {
cfg: BindingConfig;
ctx: BindingMsgContext;
command?: { to?: string | null; senderId?: string | null };
sessionKey?: string | null;
parentSessionKey?: string | null;
}) =>
resolveConversationBindingContextFromMessage({
cfg: params.cfg,
ctx: {
...params.ctx,
SenderId: params.command?.senderId ?? params.ctx.SenderId,
SessionKey: params.sessionKey ?? params.ctx.SessionKey,
ParentSessionKey: params.parentSessionKey ?? params.ctx.ParentSessionKey,
To: params.command?.to ?? params.ctx.To,
},
}),
resolveConversationBindingContextFromMessage,
resolveConversationBindingThreadIdFromMessage: (ctx: BindingMsgContext) => resolveThreadId(ctx),
};
});
const threadInfoMocks = vi.hoisted(() => ({
parseSessionThreadInfo: vi.fn<
(sessionKey: string | undefined) => {
@@ -345,6 +446,18 @@ vi.mock("./reply-media-paths.runtime.js", () => ({
vi.mock("../../agents/runtime-plugins.js", () => ({
ensureRuntimePluginsLoaded: runtimePluginMocks.ensureRuntimePluginsLoaded,
}));
vi.mock("./conversation-binding-input.js", () => ({
resolveConversationBindingAccountIdFromMessage:
conversationBindingMocks.resolveConversationBindingAccountIdFromMessage,
resolveConversationBindingChannelFromMessage:
conversationBindingMocks.resolveConversationBindingChannelFromMessage,
resolveConversationBindingContextFromAcpCommand:
conversationBindingMocks.resolveConversationBindingContextFromAcpCommand,
resolveConversationBindingContextFromMessage:
conversationBindingMocks.resolveConversationBindingContextFromMessage,
resolveConversationBindingThreadIdFromMessage:
conversationBindingMocks.resolveConversationBindingThreadIdFromMessage,
}));
vi.mock("../../tts/status-config.js", () => ({
resolveStatusTtsSnapshot: () => ({
autoMode: "always",
@@ -771,7 +884,8 @@ describe("dispatchReplyFromConfig", () => {
OriginatingTo: undefined,
});
const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload;
const replyResolver = async () =>
({ text: "hi", mediaUrl: "https://example.test/reply.png" }) satisfies ReplyPayload;
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();

View File

@@ -432,26 +432,38 @@ export async function dispatchReplyFromConfig(
});
const routeReplyTo = replyRoute.to;
const deliveryChannel = shouldRouteToOriginating ? routeReplyChannel : currentSurface;
const { createReplyMediaPathNormalizer } = await loadReplyMediaPathsRuntime();
const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg,
sessionKey: acpDispatchSessionKey,
workspaceDir,
messageProvider: deliveryChannel,
accountId: replyRoute.accountId,
groupId,
groupChannel: ctx.GroupChannel,
groupSpace: ctx.GroupSpace,
requesterSenderId: ctx.SenderId,
requesterSenderName: ctx.SenderName,
requesterSenderUsername: ctx.SenderUsername,
requesterSenderE164: ctx.SenderE164,
});
let normalizeReplyMediaPaths:
| ReturnType<
(typeof import("./reply-media-paths.runtime.js"))["createReplyMediaPathNormalizer"]
>
| undefined;
const getNormalizeReplyMediaPaths = async () => {
if (normalizeReplyMediaPaths) {
return normalizeReplyMediaPaths;
}
const { createReplyMediaPathNormalizer } = await loadReplyMediaPathsRuntime();
normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg,
sessionKey: acpDispatchSessionKey,
workspaceDir,
messageProvider: deliveryChannel,
accountId: replyRoute.accountId,
groupId,
groupChannel: ctx.GroupChannel,
groupSpace: ctx.GroupSpace,
requesterSenderId: ctx.SenderId,
requesterSenderName: ctx.SenderName,
requesterSenderUsername: ctx.SenderUsername,
requesterSenderE164: ctx.SenderE164,
});
return normalizeReplyMediaPaths;
};
const normalizeReplyMediaPayload = async (payload: ReplyPayload): Promise<ReplyPayload> => {
if (!resolveSendableOutboundReplyParts(payload).hasMedia) {
return payload;
}
return await normalizeReplyMediaPaths(payload);
const normalizeReplyMediaPayloadPaths = await getNormalizeReplyMediaPaths();
return await normalizeReplyMediaPayloadPaths(payload);
};
const routeReplyToOriginating = async (