feat(FN-3751): make wake dispatch fire-and-forget to prevent request hangs

Merges FN-3751 mailbox wake dispatch (fire-and-forget fix, async coverage tests, and gate verification) with FN-3748 workspace aliases for `@fusion/*` packages in desktop vite config. The bulk of file changes are routing refactors and agent route tests supporting the wake mechanism, plus a minor ver

Fusion-Task-Id: FN-3751
This commit is contained in:
Fusion
2026-05-08 11:14:09 -07:00
committed by gsxdsm
parent bd0c8e15eb
commit ceb113c680
3 changed files with 91 additions and 30 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix mailbox composer Send button hanging when "Wake agent immediately" is checked. The /api/messages route now dispatches the wake heartbeat asynchronously so the UI returns immediately after the message is stored.

View File

@@ -3210,10 +3210,58 @@ describe("Messaging Routes", () => {
);
expect(res.status).toBe(201);
expect(executeHeartbeat).toHaveBeenCalledWith({
agentId: "agent-wake-1",
source: "on_demand",
triggerDetail: "wake-on-message",
await vi.waitFor(() => {
expect(executeHeartbeat).toHaveBeenCalledWith({
agentId: "agent-wake-1",
source: "on_demand",
triggerDetail: "wake-on-message",
});
});
});
it("FN-3751: returns 201 immediately without waiting for executeHeartbeat to resolve", async () => {
let heartbeatResolved = false;
const executeHeartbeat = vi.fn().mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => {
heartbeatResolved = true;
resolve({ id: "run-delayed" });
}, 500);
}),
);
const wakeApp = express();
wakeApp.use(express.json());
wakeApp.use("/api", createApiRoutes(store, {
heartbeatMonitor: { executeHeartbeat, rootDir } as any,
}));
const startedAt = Date.now();
const res = await REQUEST(
wakeApp,
"POST",
"/api/messages",
JSON.stringify({
toId: "agent-wake-fast",
toType: "agent",
content: "wake without blocking send",
type: "user-to-agent",
wakeImmediately: true,
}),
{ "Content-Type": "application/json" },
);
const elapsedMs = Date.now() - startedAt;
expect(res.status).toBe(201);
expect(elapsedMs).toBeLessThan(100);
expect(heartbeatResolved).toBe(false);
await vi.waitFor(() => {
expect(executeHeartbeat).toHaveBeenCalledWith({
agentId: "agent-wake-fast",
source: "on_demand",
triggerDetail: "wake-on-message",
});
});
});
@@ -3327,7 +3375,9 @@ describe("Messaging Routes", () => {
expect(res.status).toBe(201);
expect(res.body.toId).toBe("agent-wake-failure");
expect(executeHeartbeat).toHaveBeenCalledTimes(1);
await vi.waitFor(() => {
expect(executeHeartbeat).toHaveBeenCalledTimes(1);
});
});
it("supports metadata.wakeRecipient as an immediate-wake request", async () => {
@@ -3353,10 +3403,12 @@ describe("Messaging Routes", () => {
);
expect(res.status).toBe(201);
expect(executeHeartbeat).toHaveBeenCalledWith({
agentId: "agent-wake-meta",
source: "on_demand",
triggerDetail: "wake-on-message",
await vi.waitFor(() => {
expect(executeHeartbeat).toHaveBeenCalledWith({
agentId: "agent-wake-meta",
source: "on_demand",
triggerDetail: "wake-on-message",
});
});
});

View File

@@ -328,29 +328,33 @@ export function registerMessagingScriptRoutes(ctx: ApiRoutesContext): void {
});
const shouldWakeImmediately = toType === "agent" && (wakeImmediately === true || metadata?.wakeRecipient === true);
if (shouldWakeImmediately) {
try {
const { store: scopedStore, projectId } = await getProjectContext(req);
const resolvedMonitor =
isHeartbeatMonitorForProject(scopedStore)
? heartbeatMonitor
: projectId
? resolveHeartbeatMonitor(scopedStore)
: undefined;
if (resolvedMonitor) {
await resolvedMonitor.executeHeartbeat({
agentId: toId,
source: "on_demand",
triggerDetail: "wake-on-message",
});
}
} catch (wakeErr) {
runtimeLogger.warn(`POST /api/messages wakeImmediately best-effort wake failed: ${wakeErr instanceof Error ? wakeErr.message : String(wakeErr)}`);
}
}
const recipientAgentId = toId;
res.status(201).json(message);
if (shouldWakeImmediately) {
void (async () => {
try {
const { store: scopedStore, projectId } = await getProjectContext(req);
const resolvedMonitor =
isHeartbeatMonitorForProject(scopedStore)
? heartbeatMonitor
: projectId
? resolveHeartbeatMonitor(scopedStore)
: undefined;
if (resolvedMonitor) {
await resolvedMonitor.executeHeartbeat({
agentId: recipientAgentId,
source: "on_demand",
triggerDetail: "wake-on-message",
});
}
} catch (wakeErr) {
runtimeLogger.warn(`POST /api/messages wakeImmediately best-effort wake failed: ${wakeErr instanceof Error ? wakeErr.message : String(wakeErr)}`);
}
})();
}
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;