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(res.status).toBe(201);
|
||||||
expect(executeHeartbeat).toHaveBeenCalledWith({
|
await vi.waitFor(() => {
|
||||||
agentId: "agent-wake-1",
|
expect(executeHeartbeat).toHaveBeenCalledWith({
|
||||||
source: "on_demand",
|
agentId: "agent-wake-1",
|
||||||
triggerDetail: "wake-on-message",
|
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.status).toBe(201);
|
||||||
expect(res.body.toId).toBe("agent-wake-failure");
|
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 () => {
|
it("supports metadata.wakeRecipient as an immediate-wake request", async () => {
|
||||||
@@ -3353,10 +3403,12 @@ describe("Messaging Routes", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(executeHeartbeat).toHaveBeenCalledWith({
|
await vi.waitFor(() => {
|
||||||
agentId: "agent-wake-meta",
|
expect(executeHeartbeat).toHaveBeenCalledWith({
|
||||||
source: "on_demand",
|
agentId: "agent-wake-meta",
|
||||||
triggerDetail: "wake-on-message",
|
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);
|
const shouldWakeImmediately = toType === "agent" && (wakeImmediately === true || metadata?.wakeRecipient === true);
|
||||||
if (shouldWakeImmediately) {
|
const recipientAgentId = toId;
|
||||||
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)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(message);
|
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) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
Reference in New Issue
Block a user