diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 0b846e8c035d..846ab88d9e85 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -2626,7 +2626,7 @@ describe("session activity performance", () => { expect(appendedEntries[1]).toBe(initialEntries[1]); }); - it("updates 20,000 ordered tool activities within 100 ms", () => { + it("updates 20,000 ordered tool activities within 250 ms", () => { const activities = Array.from({ length: 20_000 }, (_, index) => makeActivity({ id: `benchmark-tool-${index}`, @@ -2663,6 +2663,8 @@ describe("session activity performance", () => { const startedAt = performance.now(); expect(deriveWorkLogEntries(updatedActivities)).toHaveLength(20_001); - expect(performance.now() - startedAt).toBeLessThan(100); + // GitHub-hosted CI has seen the 100ms #8006 budget fail at ~139ms on the + // same tree that passed PR CI. 250ms still fails a quadratic rebuild. + expect(performance.now() - startedAt).toBeLessThan(250); }); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index d4a35c3f6c28..cc6a3bf76fdf 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -1029,10 +1029,22 @@ function isAgentInternalActivity(activity: OrchestrationThreadActivity): boolean return typeof payload.agentId === "string" && payload.agentId.trim().length > 0; } +/** Streaming appends are already ordered; skip the n log n copy+sort on that path. */ +function orderActivities( + activities: ReadonlyArray, +): ReadonlyArray { + for (let index = 1; index < activities.length; index++) { + if (compareActivitiesByOrder(activities[index - 1]!, activities[index]!) > 0) { + return activities.toSorted(compareActivitiesByOrder); + } + } + return activities; +} + export function deriveWorkLogEntries( activities: ReadonlyArray, ): WorkLogEntry[] { - const ordered = [...activities].toSorted(compareActivitiesByOrder); + const ordered = orderActivities(activities); const entries: DerivedWorkLogEntry[] = []; // Answers arrive in a separate activity from the questions; fold them back // into the entry that asked, so one round trip renders as one Q&A card.