fix(ui): terminal messages no longer reload the session roster (#128397)

* perf(ui): avoid redundant terminal roster refreshes

* fix(ui): preserve filtered roster refreshes

* fix(ui): defer filtered roster snapshots

* fix(ui): classify roster snapshot eligibility

* fix(ui): fence terminal roster membership
This commit is contained in:
Peter Steinberger
2026-08-23 23:04:23 -07:00
committed by GitHub
parent 6ce272c2a6
commit 91dc6e102a
5 changed files with 264 additions and 17 deletions
+1 -1
View File
@@ -4104,7 +4104,7 @@ ui/src/lib/plugin-activation.ts 3
ui/src/lib/session-pull-requests.ts 2
ui/src/lib/sessions/custom-groups.ts 5
ui/src/lib/sessions/grouping.ts 4
ui/src/lib/sessions/index.ts 2
ui/src/lib/sessions/index.ts 1
ui/src/lib/sessions/navigation.ts 1
ui/src/lib/sessions/reconcile.ts 10
ui/src/lib/sessions/session-key.ts 4
@@ -6,6 +6,7 @@ import {
chatSessionListResponse,
createChatFlowE2eSuite,
expectDefined,
expectRequestCountStable,
installMockGateway,
pauseVirtualClock,
requireRecord,
@@ -205,13 +206,12 @@ suite.define(() => {
},
messageId: "terminal-sidebar-reply",
messageSeq: 2,
session: expectDefined(completed.sessions[0], "completed sidebar session fixture"),
sessionKey: key,
status: "done",
});
await expect
.poll(async () => (await gateway.getRequests("sessions.list")).length)
.toBeGreaterThan(listCount);
await row.getByText("Repair landed cleanly").waitFor();
await expectRequestCountStable(gateway, "sessions.list", listCount);
expect(await row.textContent()).not.toContain("[[");
if (captureUiProofEnabled) {
await page.screenshot({
+224 -3
View File
@@ -245,12 +245,27 @@ describe("event-driven session list refresh", () => {
emitEvent({
type: "event",
event: "session.message",
payload: { sessionKey: key, updatedAt: 1, status: "done" },
payload: {
sessionKey: key,
hasActiveRun: false,
status: "done",
session: {
key,
kind: "direct",
updatedAt: 2,
hasActiveRun: false,
status: "done",
},
},
});
await vi.advanceTimersByTimeAsync(SESSION_EVENT_REFRESH_DEBOUNCE_MS);
expect(request).toHaveBeenCalledTimes(2);
expect(calls).toEqual({ canonical: 2, main: 2, research: 1 });
expect(request).toHaveBeenCalledTimes(1);
expect(request).toHaveBeenCalledWith(
"sessions.list",
expect.objectContaining({ agentId: "main", includeUnknown: false }),
);
expect(calls).toEqual({ canonical: 1, main: 2, research: 1 });
expect(sessions.state.result?.sessions[0]).toMatchObject({
key,
hasActiveRun: false,
@@ -269,6 +284,212 @@ describe("event-driven session list refresh", () => {
}
});
it("refreshes the configured-only roster for a terminal snapshot outside its last list", async () => {
vi.useFakeTimers();
const mainRow = { key: "agent:main:main", kind: "direct" as const, updatedAt: 1 };
const request = vi.fn(async (method: string) => {
if (method !== "sessions.list") {
throw new Error(`Unexpected request: ${method}`);
}
return sessionsResult(1, [mainRow]);
});
const { sessions, emitEvent } = createHarness(
request as unknown as GatewayBrowserClient["request"],
);
try {
await sessions.refresh({ force: true });
expect(request).toHaveBeenCalledWith(
"sessions.list",
expect.objectContaining({ configuredAgentsOnly: true }),
);
request.mockClear();
emitEvent({
type: "event",
event: "session.message",
payload: {
agentId: "local",
sessionKey: "agent:local:main",
hasActiveRun: false,
status: "done",
session: {
key: "agent:local:main",
kind: "direct",
updatedAt: 2,
archived: false,
hasActiveRun: false,
status: "done",
},
},
});
expect(sessions.state.result?.sessions).toEqual([mainRow]);
await vi.advanceTimersByTimeAsync(SESSION_EVENT_REFRESH_DEBOUNCE_MS);
expect(request).toHaveBeenCalledExactlyOnceWith(
"sessions.list",
expect.objectContaining({ configuredAgentsOnly: true }),
);
expect(sessions.state.result?.sessions).toEqual([mainRow]);
} finally {
sessions.dispose();
vi.useRealTimers();
}
});
it("keeps an archived terminal session until the Gateway replaces the active roster", async () => {
vi.useFakeTimers();
const mainRow = { key: "agent:main:main", kind: "direct" as const, updatedAt: 1 };
const fallbackRow = { key: "agent:main:fallback", kind: "direct" as const, updatedAt: 2 };
let listCalls = 0;
const request = vi.fn(async (method: string) => {
if (method !== "sessions.list") {
throw new Error(`Unexpected request: ${method}`);
}
listCalls += 1;
return sessionsResult(listCalls, listCalls === 1 ? [mainRow] : [fallbackRow]);
});
const { sessions, emitEvent } = createHarness(
request as unknown as GatewayBrowserClient["request"],
);
try {
await sessions.refresh({ force: true });
request.mockClear();
const visibleRosters: SessionsListResult["sessions"][] = [];
let previousRoster = sessions.state.result?.sessions;
const unsubscribe = sessions.subscribe((next) => {
if (next.result?.sessions !== previousRoster) {
previousRoster = next.result?.sessions;
visibleRosters.push(next.result?.sessions ?? []);
}
});
emitEvent({
type: "event",
event: "session.message",
payload: {
sessionKey: mainRow.key,
hasActiveRun: false,
status: "done",
session: {
...mainRow,
updatedAt: 3,
archived: true,
hasActiveRun: false,
status: "done",
},
},
});
expect(sessions.state.result?.sessions).toEqual([mainRow]);
expect(visibleRosters).toEqual([]);
await vi.advanceTimersByTimeAsync(SESSION_EVENT_REFRESH_DEBOUNCE_MS);
expect(request).toHaveBeenCalledExactlyOnceWith(
"sessions.list",
expect.objectContaining({ configuredAgentsOnly: true }),
);
expect(visibleRosters).toEqual([[fallbackRow]]);
expect(sessions.state.result?.sessions).toEqual([fallbackRow]);
unsubscribe();
} finally {
sessions.dispose();
vi.useRealTimers();
}
});
it.each([
{
filter: "ownerId",
query: { ownerId: "profile-ada" },
applyFilter: (sessions: ReturnType<typeof createSessionCapability>) =>
sessions.setOwnerFilter("profile-ada"),
},
{
filter: "involvingMe",
query: { involvingMe: true },
applyFilter: (sessions: ReturnType<typeof createSessionCapability>) =>
sessions.setInvolvingMeFilter(true),
},
{
filter: "search",
query: { search: "Ada" },
applyFilter: (sessions: ReturnType<typeof createSessionCapability>) =>
sessions.refresh({ agentId: "main", search: "Ada", force: true }),
},
])(
"refreshes the $filter-filtered primary roster after a terminal session message",
async ({ applyFilter, query }) => {
vi.useFakeTimers();
const key = "agent:main:main";
let matchesFilteredRoster = true;
const row = {
key,
kind: "direct" as const,
updatedAt: 1,
hasActiveRun: true,
status: "running" as const,
label: "Ada",
owner: { actor: { type: "human" as const, id: "profile-ada", label: "Ada" } },
};
const request = vi.fn(
async (
method: string,
params?: { ownerId?: string; involvingMe?: boolean; search?: string },
) => {
if (method !== "sessions.list") {
throw new Error(`Unexpected request: ${method}`);
}
const filtered = Boolean(params?.ownerId || params?.involvingMe || params?.search);
return sessionsResult(
matchesFilteredRoster ? 1 : 2,
filtered && !matchesFilteredRoster ? [] : [row],
);
},
);
const { sessions, emitEvent } = createHarness(
request as unknown as GatewayBrowserClient["request"],
);
try {
await sessions.refresh({ agentId: "main", force: true });
await applyFilter(sessions);
expect(sessions.state.result?.sessions.map((session) => session.key)).toEqual([key]);
request.mockClear();
matchesFilteredRoster = false;
emitEvent({
type: "event",
event: "session.message",
payload: {
sessionKey: key,
hasActiveRun: false,
status: "done",
session: {
...row,
updatedAt: 2,
hasActiveRun: false,
status: "done",
label: "Bob",
owner: { actor: { type: "human", id: "profile-bob", label: "Bob" } },
},
},
});
expect(sessions.state.result?.sessions).toEqual([row]);
await vi.advanceTimersByTimeAsync(SESSION_EVENT_REFRESH_DEBOUNCE_MS);
expect(request).toHaveBeenCalledTimes(1);
expect(request).toHaveBeenCalledWith("sessions.list", expect.objectContaining(query));
expect(sessions.state.result?.sessions).toEqual([]);
} finally {
sessions.dispose();
vi.useRealTimers();
}
},
);
it("retains every loaded page when a session event replaces the canonical list", async () => {
vi.useFakeTimers();
const rows = Array.from({ length: 120 }, (_, index) => ({
+30 -8
View File
@@ -415,22 +415,44 @@ export function createSessionCapability(gateway: SessionGateway): SessionCapabil
resultAgentId: state.agentId,
archivedFilter: roster.lastOptions().archivedFilter,
});
if (eventInfo?.archived !== null) {
const payload = event.payload as {
agentId?: unknown;
reason?: unknown;
session?: unknown;
} | null;
const hasActiveRun = reconciled.hasActiveRun ?? eventInfo?.hasActiveRun;
const status = reconciled.status ?? eventInfo?.status;
const runEnded =
hasActiveRun === false || (status !== null && status !== undefined && status !== "running");
const isTerminalMessage = event.event === "session.message" && runEnded;
// Only an existing Gateway roster member that remains active can be replaced directly.
const primarySnapshotApplied =
isTerminalMessage &&
reconciled.applied &&
eventInfo !== null &&
eventInfo.archived !== true &&
typeof payload?.session === "object" &&
payload.session !== null &&
roster.canApplyPrimarySnapshot() &&
state.result?.sessions.some((row) =>
uiSessionEventMatches(
{ ...gateway.snapshot, sessionKey: row.key },
eventInfo.key,
eventInfo.agentId,
),
) === true;
if ((eventInfo?.archived !== null && !isTerminalMessage) || primarySnapshotApplied) {
const result = decorateRows(reconciled.result);
if (result !== state.result) {
publishReconciledState({ ...state, result });
}
}
const eventReason = (event.payload as { reason?: unknown } | null)?.reason;
const payloadAgentId = (event.payload as { agentId?: unknown } | null)?.agentId;
const eventReason = payload?.reason;
const payloadAgentId = payload?.agentId;
if (eventReason === "groups") {
groups.invalidate();
void groups.load();
}
const hasActiveRun = reconciled.hasActiveRun ?? eventInfo?.hasActiveRun;
const status = reconciled.status ?? eventInfo?.status;
const runEnded =
hasActiveRun === false || (status !== null && status !== undefined && status !== "running");
if (event.event === "session.message" && !runEnded) {
return;
}
@@ -463,12 +485,12 @@ export function createSessionCapability(gateway: SessionGateway): SessionCapabil
publish({ ...state, deletedSessions: remainingDeletedSessions });
}
}
// Gateway lists own filtering/order; authoritative events invalidate every matching roster.
roster.scheduleEvent({
agentId:
eventInfo?.agentId ??
parseAgentSessionKey(eventInfo?.key)?.agentId ??
(typeof payloadAgentId === "string" ? payloadAgentId : undefined),
primarySnapshotApplied,
});
});
@@ -552,8 +552,12 @@ export function createSessionRosterRefresh(host: SessionRosterRefreshHost) {
return refresh({ ...options, force: true });
},
lastOptions: () => lastListOptions,
scheduleEvent(options: { agentId?: string | null } = {}) {
eventRefreshCoordinator.schedule();
// Gateway-owned membership filters require an authoritative list refresh.
canApplyPrimarySnapshot: () => isPrimarySessionListQuery(lastListOptions),
scheduleEvent(options: { agentId?: string | null; primarySnapshotApplied?: boolean } = {}) {
if (!options.primarySnapshotApplied) {
eventRefreshCoordinator.schedule();
}
const agentId = options.agentId ? normalizeAgentId(options.agentId) : null;
for (const entry of managedLists.values()) {
const queryAgentId = managedSessionListAgentId(entry);