feat(FN-3484): normalize dashboard mailbox and user identity for messaging
This merge normalizes dashboard user identity and mailbox messaging (FN-3484, 4 steps), adds workflow step execution for plugins (FN-3490), and updates the restart integration store mock for plugin templates (FN-3096). Core changes touch the message store and store modules with identity normalizatio Fusion-Task-Id: FN-3484
This commit is contained in:
@@ -727,6 +727,24 @@ describe("createSendMessageTool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["dashboard", "user:dashboard", "User: user:dashboard"])(
|
||||
"canonicalizes dashboard alias '%s' for agent-to-user sends",
|
||||
async (dashboardAlias) => {
|
||||
const mockMessage = createMessage({ toId: "dashboard", toType: "user", type: "agent-to-user" });
|
||||
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
|
||||
|
||||
await executeTool(tool, {
|
||||
to_id: dashboardAlias,
|
||||
content: "Status",
|
||||
type: "agent-to-user",
|
||||
});
|
||||
|
||||
expect(messageStore.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ toId: "dashboard", toType: "user", type: "agent-to-user" }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("maps recipient type to agent for agent-to-agent messages", async () => {
|
||||
const mockMessage = createMessage({ toType: "agent", type: "agent-to-agent" });
|
||||
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
|
||||
|
||||
@@ -1607,22 +1607,26 @@ describe("executeHeartbeat", () => {
|
||||
const sendMessageTool = callArgs.customTools?.find((tool: { name: string }) => tool.name === "fn_send_message");
|
||||
expect(sendMessageTool).toBeDefined();
|
||||
|
||||
await sendMessageTool!.execute(
|
||||
"tool-call",
|
||||
{
|
||||
to_id: "dashboard",
|
||||
content: "Status: I am on it.",
|
||||
type: "agent-to-user",
|
||||
reply_to_message_id: inboundFromUser.id,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
{} as any,
|
||||
);
|
||||
for (const [index, alias] of ["dashboard", "user:dashboard", "User: user:dashboard"].entries()) {
|
||||
await sendMessageTool!.execute(
|
||||
`tool-call-${index}`,
|
||||
{
|
||||
to_id: alias,
|
||||
content: `Status: I am on it. (${index})`,
|
||||
type: "agent-to-user",
|
||||
reply_to_message_id: inboundFromUser.id,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
{} as any,
|
||||
);
|
||||
}
|
||||
|
||||
const dashboardInbox = fakeMessageStore.getInbox("dashboard", "user");
|
||||
const linkedReply = dashboardInbox.find((message) => message.content === "Status: I am on it.");
|
||||
expect(linkedReply?.metadata).toEqual({ replyTo: { messageId: inboundFromUser.id } });
|
||||
for (const index of [0, 1, 2]) {
|
||||
const linkedReply = dashboardInbox.find((message) => message.content === `Status: I am on it. (${index})`);
|
||||
expect(linkedReply?.metadata).toEqual({ replyTo: { messageId: inboundFromUser.id } });
|
||||
}
|
||||
|
||||
await monitor.stop();
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { existsSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore } from "@fusion/core";
|
||||
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
|
||||
import { DASHBOARD_USER_ID, dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
|
||||
import { ResearchOrchestrator } from "./research-orchestrator.js";
|
||||
import { ResearchProviderRegistry } from "./research/provider-registry.js";
|
||||
import { ResearchStepRunner } from "./research-step-runner.js";
|
||||
@@ -1432,7 +1432,10 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
|
||||
|
||||
try {
|
||||
const messageType = params.type ?? "agent-to-agent";
|
||||
const recipientType = messageType === "agent-to-user" ? "user" : "agent";
|
||||
const recipientType: "user" | "agent" = messageType === "agent-to-user" ? "user" : "agent";
|
||||
const recipient = recipientType === "user"
|
||||
? normalizeMessageParticipant(params.to_id, recipientType)
|
||||
: { id: params.to_id, type: recipientType };
|
||||
const replyToMessageId = params.reply_to_message_id?.trim();
|
||||
|
||||
if (params.reply_to_message_id !== undefined && !replyToMessageId) {
|
||||
@@ -1445,8 +1448,8 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
|
||||
const message = messageStore.sendMessage({
|
||||
fromId: fromAgentId,
|
||||
fromType: "agent",
|
||||
toId: params.to_id,
|
||||
toType: recipientType,
|
||||
toId: recipient.id,
|
||||
toType: recipient.type,
|
||||
content,
|
||||
type: messageType,
|
||||
...(replyToMessageId ? { metadata: { replyTo: { messageId: replyToMessageId } } } : {}),
|
||||
@@ -1455,7 +1458,7 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `Message sent to ${params.to_id} (ID: ${message.id})`,
|
||||
text: `Message sent to ${recipient.id === DASHBOARD_USER_ID ? DASHBOARD_USER_ID : params.to_id} (ID: ${message.id})`,
|
||||
}],
|
||||
details: { messageId: message.id },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user