feat(FN-4177): add heartbeat messaging support for chat rooms
The merge delivers room heartbeat messaging for agents, allowing heartbeat-triggered agents to send messages into chat rooms and subscribe to room activity. Core changes include new chat-store room APIs, engine heartbeat room message tools, and corresponding tests, with a small dashboard ChatView up Fusion-Task-Id: FN-4177
This commit is contained in:
@@ -104,6 +104,48 @@ describe("ChatStore — rooms (FN-3805..FN-3811 contract)", () => {
|
||||
expect(updated.attachments?.[0]?.id).toBe("att-room");
|
||||
});
|
||||
|
||||
it("returns only messages after sinceIso", async () => {
|
||||
const room = store.createRoom({ name: "since-test" });
|
||||
store.addRoomMessage(room.id, { role: "user", content: "before" });
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const sinceIso = new Date().toISOString();
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const after = store.addRoomMessage(room.id, { role: "user", content: "after" });
|
||||
|
||||
expect(store.listRoomMessagesSince(room.id, sinceIso).map((message) => message.id)).toEqual([after.id]);
|
||||
});
|
||||
|
||||
it("excludes authored agent messages when excludeSenderAgentId is set", async () => {
|
||||
const room = store.createRoom({ name: "exclude-self" });
|
||||
store.addRoomMessage(room.id, { role: "assistant", content: "own", senderAgentId: "agent-1" });
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const other = store.addRoomMessage(room.id, { role: "assistant", content: "other", senderAgentId: "agent-2" });
|
||||
const user = store.addRoomMessage(room.id, { role: "user", content: "user" });
|
||||
|
||||
expect(
|
||||
store.listRoomMessagesSince(room.id, "1970-01-01T00:00:00.000Z", { excludeSenderAgentId: "agent-1" }).map((message) => message.id),
|
||||
).toEqual([other.id, user.id]);
|
||||
});
|
||||
|
||||
it("respects the limit cap", async () => {
|
||||
const room = store.createRoom({ name: "limit-test" });
|
||||
store.addRoomMessage(room.id, { role: "user", content: "one" });
|
||||
store.addRoomMessage(room.id, { role: "user", content: "two" });
|
||||
store.addRoomMessage(room.id, { role: "user", content: "three" });
|
||||
|
||||
expect(store.listRoomMessagesSince(room.id, "1970-01-01T00:00:00.000Z", { limit: 2 }).map((message) => message.content)).toEqual([
|
||||
"one",
|
||||
"two",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns empty when there are no new room messages", () => {
|
||||
const room = store.createRoom({ name: "empty-test" });
|
||||
store.addRoomMessage(room.id, { role: "user", content: "old" });
|
||||
|
||||
expect(store.listRoomMessagesSince(room.id, new Date().toISOString())).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps cross-room and direct-vs-room histories isolated", () => {
|
||||
const session = store.createSession({ agentId: "agent-1" });
|
||||
store.addMessage(session.id, { role: "user", content: "direct" });
|
||||
|
||||
@@ -936,6 +936,29 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
return rows.map((row) => this.rowToRoomMessage(row));
|
||||
}
|
||||
|
||||
listRoomMessagesSince(
|
||||
roomId: string,
|
||||
sinceIso: string,
|
||||
options?: { excludeSenderAgentId?: string; limit?: number },
|
||||
): ChatRoomMessage[] {
|
||||
const whereClauses: string[] = ["roomId = ?", "createdAt > ?"];
|
||||
const params: Array<string | number | null> = [roomId, sinceIso];
|
||||
|
||||
if (options?.excludeSenderAgentId) {
|
||||
whereClauses.push("(senderAgentId IS NULL OR senderAgentId != ?)");
|
||||
params.push(options.excludeSenderAgentId);
|
||||
}
|
||||
|
||||
const rows = this.db.prepare(`
|
||||
SELECT * FROM chat_room_messages
|
||||
WHERE ${whereClauses.join(" AND ")}
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT ?
|
||||
`).all(...params, options?.limit ?? 50) as ChatRoomMessageRow[];
|
||||
|
||||
return rows.map((row) => this.rowToRoomMessage(row));
|
||||
}
|
||||
|
||||
getRoomMessage(id: string): ChatRoomMessage | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM chat_room_messages WHERE id = ?").get(id) as ChatRoomMessageRow | undefined;
|
||||
return row ? this.rowToRoomMessage(row) : undefined;
|
||||
|
||||
Reference in New Issue
Block a user