feat(FN-1976): forward mailbox message events end to end

- Wire MessageStore into the in-process executor runtime and project engine
- Forward MessageStore events through dashboard SSE infrastructure for mailbox updates
- Close mailbox pipeline gaps across API routes, server wiring, and mailbox UI components
- Add regression coverage for messaging routes, SSE forwarding, and agent tool behavior
- Document the MessageStore SSE wiring pattern in .fusion/memory.md
This commit is contained in:
Fusion
2026-04-16 16:32:56 -07:00
committed by gsxdsm
parent fc33ea89ee
commit bede73c81d
13 changed files with 624 additions and 41 deletions

View File

@@ -104,8 +104,8 @@ describe("createSendMessageTool", () => {
);
});
it("uses provided type when specified", async () => {
const mockMessage = createMessage();
it("uses provided type when specified and maps recipient type for agent-to-user", async () => {
const mockMessage = createMessage({ toType: "user", type: "agent-to-user" });
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
await executeTool(tool, {
@@ -115,7 +115,22 @@ describe("createSendMessageTool", () => {
});
expect(messageStore.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ type: "agent-to-user" })
expect.objectContaining({ type: "agent-to-user", toType: "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);
await executeTool(tool, {
to_id: "agent-2",
content: "Test",
type: "agent-to-agent",
});
expect(messageStore.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ type: "agent-to-agent", toType: "agent" })
);
});

View File

@@ -68,7 +68,7 @@ export const delegateTaskParams = Type.Object({
});
export const sendMessageParams = Type.Object({
to_id: Type.String({ description: "Recipient agent ID (e.g. 'agent-abc123')" }),
to_id: Type.String({ description: "Recipient ID (agent ID or user ID, depending on message type)" }),
content: Type.String({ description: "Message body (1-2000 characters)" }),
type: Type.Optional(Type.Union([
Type.Literal("agent-to-agent"),
@@ -474,13 +474,16 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
}
try {
const messageType = params.type ?? "agent-to-agent";
const recipientType = messageType === "agent-to-user" ? "user" : "agent";
const message = messageStore.sendMessage({
fromId: fromAgentId,
fromType: "agent",
toId: params.to_id,
toType: "agent",
toType: recipientType,
content,
type: params.type ?? "agent-to-agent",
type: messageType,
});
return {

View File

@@ -268,6 +268,11 @@ export class ProjectEngine {
return this.runtime.getAgentStore();
}
/** Get the MessageStore (if initialized). Returns undefined before start(). */
getMessageStore(): import("@fusion/core").MessageStore | undefined {
return this.runtime.getMessageStore();
}
/** Get the HeartbeatMonitor (if initialized). */
getHeartbeatMonitor() {
return this.runtime.getHeartbeatMonitor();

View File

@@ -131,7 +131,12 @@ export class InProcessRuntime
try {
// 1. Initialize TaskStore (use external if provided, otherwise create new)
const { TaskStore, PluginStore: PluginStoreClass, PluginLoader: PluginLoaderClass } = await import("@fusion/core");
const {
TaskStore,
PluginStore: PluginStoreClass,
PluginLoader: PluginLoaderClass,
MessageStore: MessageStoreClass,
} = await import("@fusion/core");
if (this.config.externalTaskStore) {
this.taskStore = this.config.externalTaskStore;
runtimeLog.log(`TaskStore provided externally for project ${this.config.projectId}`);
@@ -141,6 +146,9 @@ export class InProcessRuntime
runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
}
// Initialize MessageStore early so TaskExecutor receives send_message capability.
this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
// 2. Initialize Plugin system (PluginStore + PluginLoader + PluginRunner)
this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
await this.pluginStore.init();
@@ -263,6 +271,7 @@ export class InProcessRuntime
usageLimitPauser: this.usageLimitPauser,
stuckTaskDetector: this.stuckTaskDetector,
pluginRunner: this.pluginRunner,
messageStore: this.messageStore,
missionStore,
onSliceComplete: (slice) => {
void this.scheduler.onSliceComplete(slice);
@@ -353,13 +362,10 @@ export class InProcessRuntime
// 6. Initialize AgentStore and HeartbeatMonitor
try {
const { AgentStore: AgentStoreClass, MessageStore: MessageStoreClass } = await import("@fusion/core");
const { AgentStore: AgentStoreClass } = await import("@fusion/core");
this.agentStore = new AgentStoreClass({ rootDir: this.taskStore.getFusionDir() });
await this.agentStore.init();
// Initialize MessageStore for wake-on-message behavior
this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
this.heartbeatMonitor = new HeartbeatMonitor({
store: this.agentStore,
agentStore: this.agentStore, // enables per-agent config resolution
@@ -718,6 +724,14 @@ export class InProcessRuntime
return this.agentStore;
}
/**
* Get the MessageStore instance (if initialized).
* Returns undefined before start() or if initialization fails.
*/
getMessageStore(): import("@fusion/core").MessageStore | undefined {
return this.messageStore;
}
/**
* Get the project's Scheduler instance.
* @throws Error if runtime has not been started