fix(pet): each user's pet tells only of their own sessions

An admin/analyst's pet counted every user's sessions (live, waiting, errors)
because their WebSocket carries everyone's. Server mode now marks their
session/agent/event frames `mine: true|false` (scope.broadcastAudience owners,
worked out once per broadcast), and GET /api/sessions?mine=1 lists only the
caller's own sessions (ownSessionWhere). The pet seeds from mine=1 and skips
frames marked mine: false. Developers and local mode are unchanged.
This commit is contained in:
2026-11-03 09:01:57 +07:00
parent 9721941962
commit c229b4b79b
9 changed files with 186 additions and 4 deletions
@@ -200,6 +200,20 @@ describe("Sagi widget", () => {
expect(within(liveChip).getByText("2")).toBeInTheDocument();
});
it("counts only the viewer's own sessions (another user's arrive marked mine: false)", () => {
renderSagi();
act(() => {
eventBus.publish({ ...sessionMsg("mine", "active"), mine: true });
eventBus.publish({ ...sessionMsg("theirs", "active"), mine: false });
eventBus.publish({ ...sessionMsg("theirs-err", "error"), mine: false });
});
fireEvent.click(screen.getByRole("button", { name: /open sagi companion/i }));
const panel = screen.getByRole("dialog", { name: /chat with sagi/i });
const liveChip = within(panel).getByText("live").closest("span.rounded-full")! as HTMLElement;
expect(within(liveChip).getByText("1")).toBeInTheDocument();
expect(screen.queryByText(/theirs/)).toBeNull();
});
it("respects the enabled preference", () => {
localStorage.setItem("agent-dashboard-sagi-enabled", "false");
renderSagi();
+6 -2
View File
@@ -7,7 +7,8 @@
* burst of the same event into one count), the pet's one-shot animation
* "moment", and imperative controls (mute, clear alerts, pat, set thinking)
* for the UI shell. It also greets once per browser session and makes an
* idle pet fidget now and then.
* idle pet fidget now and then. It only ever counts the viewer's OWN sessions
* (`mine=1` seed, and frames marked `mine: false` skipped).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
@@ -278,7 +279,7 @@ export function useSagiBrain(): SagiBrain {
}, GREET_DELAY_MS);
};
api.sessions
.list({ status: "active", limit: 100 })
.list({ status: "active", limit: 100, mine: true })
.then((res) => {
if (cancelled) return;
update((prev) => seedSessions(prev, res.sessions, Date.now()));
@@ -294,6 +295,9 @@ export function useSagiBrain(): SagiBrain {
// Subscribe to the live stream and connection status.
useEffect(() => {
const unsubMsg = eventBus.subscribe((msg: WSMessage) => {
// Each user's pet tells only of their own sessions: an admin/analyst
// gets every user's, marked `mine: false` when not theirs.
if (msg.mine === false) return;
const prev = stateRef.current;
const { state: next, pulse, subject } = reduceSagi(prev, msg, Date.now());
// Most frames only move lastActivityAt, which the moods read at a 1 s
+3
View File
@@ -1858,6 +1858,8 @@ export const api = {
archived?: boolean;
/** Pinned sessions first (by pin time), before the limit. */
pinnedFirst?: boolean;
/** Server mode: the caller's OWN sessions, even for an admin/analyst (`mine=1`). */
mine?: boolean;
/** Only active sessions waiting on the user (`awaiting=1`). */
awaiting?: boolean;
/** ISO bounds on `started_at`. */
@@ -1878,6 +1880,7 @@ export const api = {
if (params?.archived !== undefined) qs.set("archived", params.archived ? "1" : "0");
if (params?.pinnedFirst) qs.set("pinned_first", "1");
if (params?.awaiting) qs.set("awaiting", "1");
if (params?.mine) qs.set("mine", "1");
if (params?.from) qs.set("from", params.from);
if (params?.to) qs.set("to", params.to);
if (params?.sort_by) qs.set("sort_by", params.sort_by);
+4
View File
@@ -1971,6 +1971,10 @@ export interface WSMessage {
/** ISO timestamp the server broadcast this message (not necessarily the
* same instant the underlying event occurred). */
timestamp: string;
/** Server mode, an admin/analyst's session/agent/event frames only: whether
* the session is their OWN (they also get every other user's). Absent for a
* developer (who only ever gets their own) and in local mode. */
mine?: boolean;
}
// ───── Session stats ─────