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:
5
.changeset/FN-3751-mailbox-wake-send-hang.md
Normal file
5
.changeset/FN-3751-mailbox-wake-send-hang.md
Normal 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.
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user