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 ca2e085d76
commit 80e49b8e40
13 changed files with 624 additions and 41 deletions

View File

@@ -1,5 +1,5 @@
import type { Request, Response } from "express";
import type { TaskStore, MissionStore, PluginStore, PluginInstallation, PluginState, AgentStore } from "@fusion/core";
import type { TaskStore, MissionStore, PluginStore, PluginInstallation, PluginState, AgentStore, MessageStore } from "@fusion/core";
import type { AiSessionStore } from "./ai-session-store.js";
let activeConnections = 0;
@@ -70,6 +70,13 @@ export type PluginLifecycleTransition =
| "uninstalled"
| "settings-updated";
/** Message event types forwarded through the SSE stream. */
export type MessageSseEventType =
| "message:sent"
| "message:received"
| "message:read"
| "message:deleted";
/**
* Normalized plugin lifecycle payload emitted via SSE.
* This is the stable contract the UI can reconcile.
@@ -173,6 +180,7 @@ export function createSSE(
pluginStore?: PluginStore,
options?: CreateSSEOptions,
agentStore?: AgentStore,
messageStore?: MessageStore,
) {
const { projectId } = options ?? {};
@@ -342,6 +350,23 @@ export function createSSE(
send(`event: agent:stateChanged\ndata: ${JSON.stringify({ id: agentId, from: fromState, to: toState })}\n\n`);
};
// --- Message event handlers ---
const onMessageSent = (message: unknown) => {
send(`event: message:sent\ndata: ${JSON.stringify(message)}\n\n`);
};
const onMessageReceived = (message: unknown) => {
send(`event: message:received\ndata: ${JSON.stringify(message)}\n\n`);
};
const onMessageRead = (message: unknown) => {
send(`event: message:read\ndata: ${JSON.stringify(message)}\n\n`);
};
const onMessageDeleted = (messageId: string) => {
send(`event: message:deleted\ndata: ${JSON.stringify({ id: messageId })}\n\n`);
};
// --- Cleanup (all handlers are defined above, safe to reference) ---
let cleaned = false;
@@ -396,6 +421,12 @@ export function createSSE(
agentStore.off("agent:deleted", onAgentDeleted);
agentStore.off("agent:stateChanged", onAgentStateChanged);
}
if (messageStore) {
messageStore.off("message:sent", onMessageSent);
messageStore.off("message:received", onMessageReceived);
messageStore.off("message:read", onMessageRead);
messageStore.off("message:deleted", onMessageDeleted);
}
};
// --- Subscribe ---
@@ -451,6 +482,13 @@ export function createSSE(
agentStore.on("agent:stateChanged", onAgentStateChanged);
}
if (messageStore) {
messageStore.on("message:sent", onMessageSent);
messageStore.on("message:received", onMessageReceived);
messageStore.on("message:read", onMessageRead);
messageStore.on("message:deleted", onMessageDeleted);
}
// Heartbeat every 30s to keep connection alive.
// Sent as a named event so the client's EventSource can detect it
// (SSE comments starting with ":" are silently consumed and never