fix(FN-3886): resolve peer agent names in mail and notifications
- Resolve participant display names in AgentDetailView mail tab for peer agents - Update MailboxModal labeling to use readable agent names instead of raw IDs - Propagate resolved agent names through notification service, notifier, and provider payloads - Add and update dashboard/engine tests plus a changeset for @runfusion/fusion Fusion-Task-Id: FN-3886
This commit is contained in:
@@ -21,6 +21,8 @@ export interface NotificationServiceOptions {
|
||||
ntfyBaseUrl?: string;
|
||||
/** Optional message store for mailbox message notifications */
|
||||
messageStore?: NotificationMessageStore;
|
||||
/** Resolve human-readable name for an agent ID used in message notifications */
|
||||
agentNameResolver?: (agentId: string) => Promise<string | null> | string | null;
|
||||
}
|
||||
|
||||
interface NotificationServiceStore {
|
||||
@@ -267,6 +269,9 @@ export class NotificationService {
|
||||
|
||||
const taskId = typeof message.metadata?.taskId === "string" ? message.metadata.taskId : undefined;
|
||||
|
||||
const fromName = await this.resolveAgentName(message.fromType, message.fromId, "from");
|
||||
const toName = await this.resolveAgentName(message.toType, message.toId, "to");
|
||||
|
||||
this.maybeNotify(message.id, eventType, {
|
||||
taskId,
|
||||
taskTitle: undefined,
|
||||
@@ -275,8 +280,10 @@ export class NotificationService {
|
||||
messageId: message.id,
|
||||
fromId: message.fromId,
|
||||
fromType: message.fromType,
|
||||
...(fromName ? { fromName } : {}),
|
||||
toId: message.toId,
|
||||
toType: message.toType,
|
||||
...(toName ? { toName } : {}),
|
||||
type: message.type,
|
||||
replyToMessageId: message.metadata?.replyTo?.messageId,
|
||||
preview,
|
||||
@@ -288,6 +295,33 @@ export class NotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveAgentName(
|
||||
participantType: Message["fromType"],
|
||||
participantId: string,
|
||||
direction: "from" | "to",
|
||||
): Promise<string | null> {
|
||||
if (participantType !== "agent") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolver = this.options.agentNameResolver;
|
||||
if (!resolver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = await resolver(participantId);
|
||||
const trimmed = typeof resolved === "string" ? resolved.trim() : "";
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
schedulerLog.log(
|
||||
`NotificationService.handleMessageSent failed to resolve ${direction} agent name agentId=${participantId} error=${message}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private setNotificationsEnabledFromSettings(settings: Settings): void {
|
||||
this.notificationsEnabled = Boolean(
|
||||
(settings.ntfyEnabled && settings.ntfyTopic) ||
|
||||
|
||||
@@ -51,6 +51,20 @@ const SUPPORTED_EVENTS = new Set<SupportedNtfyEvent>([
|
||||
"message:agent-to-agent",
|
||||
]);
|
||||
|
||||
export function resolveParticipantLabel(
|
||||
metadata: NotificationPayload["metadata"] | undefined,
|
||||
kind: "from" | "to",
|
||||
): string {
|
||||
const nameKey = kind === "from" ? "fromName" : "toName";
|
||||
const idKey = kind === "from" ? "fromId" : "toId";
|
||||
const name = typeof metadata?.[nameKey] === "string" ? metadata[nameKey].trim() : "";
|
||||
if (name.length > 0) {
|
||||
return name;
|
||||
}
|
||||
const id = typeof metadata?.[idKey] === "string" ? metadata[idKey].trim() : "";
|
||||
return id.length > 0 ? id : kind === "from" ? "agent" : "recipient";
|
||||
}
|
||||
|
||||
export class NtfyNotificationProvider implements NotificationProvider {
|
||||
private config?: NtfyProviderConfig;
|
||||
private abortController: AbortController | null = null;
|
||||
@@ -113,8 +127,8 @@ export class NtfyNotificationProvider implements NotificationProvider {
|
||||
|
||||
const identifier = formatTaskIdentifier(taskLike);
|
||||
const messageId = typeof payload.metadata?.messageId === "string" ? payload.metadata.messageId : undefined;
|
||||
const senderLabel = typeof payload.metadata?.fromId === "string" ? payload.metadata.fromId : "agent";
|
||||
const recipientLabel = typeof payload.metadata?.toId === "string" ? payload.metadata.toId : "recipient";
|
||||
const senderLabel = resolveParticipantLabel(payload.metadata, "from");
|
||||
const recipientLabel = resolveParticipantLabel(payload.metadata, "to");
|
||||
const preview = typeof payload.metadata?.preview === "string"
|
||||
? payload.metadata.preview
|
||||
: "(no preview)";
|
||||
|
||||
@@ -20,6 +20,20 @@ export interface WebhookProviderConfig {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
function resolveParticipantLabel(
|
||||
metadata: NotificationPayload["metadata"] | undefined,
|
||||
kind: "from" | "to",
|
||||
): string {
|
||||
const nameKey = kind === "from" ? "fromName" : "toName";
|
||||
const idKey = kind === "from" ? "fromId" : "toId";
|
||||
const name = typeof metadata?.[nameKey] === "string" ? metadata[nameKey].trim() : "";
|
||||
if (name.length > 0) {
|
||||
return name;
|
||||
}
|
||||
const id = typeof metadata?.[idKey] === "string" ? metadata[idKey].trim() : "";
|
||||
return id.length > 0 ? id : kind === "from" ? "agent" : "recipient";
|
||||
}
|
||||
|
||||
export class WebhookNotificationProvider implements NotificationProvider {
|
||||
private config: WebhookProviderConfig | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
@@ -142,6 +156,17 @@ export class WebhookNotificationProvider implements NotificationProvider {
|
||||
return "Pipeline gridlocked";
|
||||
case "fallback-used":
|
||||
return `Fusion recovered by switching from ${String(payload.metadata?.primaryModel ?? "primary model")} to ${String(payload.metadata?.fallbackModel ?? "fallback model")} (${String(payload.metadata?.triggerPoint ?? "unknown trigger")})`;
|
||||
case "message:agent-to-user": {
|
||||
const from = resolveParticipantLabel(payload.metadata, "from");
|
||||
const preview = typeof payload.metadata?.preview === "string" ? payload.metadata.preview : "(no preview)";
|
||||
return `From: ${from} → You: ${preview}`;
|
||||
}
|
||||
case "message:agent-to-agent": {
|
||||
const from = resolveParticipantLabel(payload.metadata, "from");
|
||||
const to = resolveParticipantLabel(payload.metadata, "to");
|
||||
const preview = typeof payload.metadata?.preview === "string" ? payload.metadata.preview : "(no preview)";
|
||||
return `From: ${from} → To: ${to}: ${preview}`;
|
||||
}
|
||||
default:
|
||||
return `Event "${event}" for task ${identifier}`;
|
||||
}
|
||||
@@ -172,6 +197,9 @@ export class WebhookNotificationProvider implements NotificationProvider {
|
||||
|
||||
const messageId = typeof payload.metadata?.messageId === "string" ? payload.metadata.messageId : undefined;
|
||||
|
||||
const fromLabel = resolveParticipantLabel(payload.metadata, "from");
|
||||
const toLabel = resolveParticipantLabel(payload.metadata, "to");
|
||||
|
||||
return {
|
||||
event: payload.event,
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -179,7 +207,15 @@ export class WebhookNotificationProvider implements NotificationProvider {
|
||||
id: payload.taskId,
|
||||
title: payload.taskTitle,
|
||||
},
|
||||
metadata: payload.metadata,
|
||||
metadata: {
|
||||
...payload.metadata,
|
||||
...(payload.event === "message:agent-to-user" || payload.event === "message:agent-to-agent"
|
||||
? {
|
||||
fromName: typeof payload.metadata?.fromName === "string" ? payload.metadata.fromName : fromLabel,
|
||||
toName: typeof payload.metadata?.toName === "string" ? payload.metadata.toName : toLabel,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
clickUrl: buildNtfyClickUrl({
|
||||
dashboardHost: this.config.dashboardHost,
|
||||
projectId: this.config.projectId,
|
||||
|
||||
Reference in New Issue
Block a user