diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index 131d277c5e6f..ae372ea938cd 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -1051,6 +1051,66 @@ describe("EnvironmentThreads", () => { expect((yield* SubscriptionRef.get(harness.threadState)).history.loading).toBe(false); }), ); + const boundary = (fields: object, id = "boundary", ordinal = 2) => ({ + ...command(id, "completed", 0, ordinal), + ...fields, + }); + const message = ( + inputIntent: "turn_start" | "steer", + createdBy: "agent" | "user", + id = "boundary", + ordinal = 2, + ) => + boundary( + { + type: "user_message", + messageId: MessageId.make(id), + inputIntent, + createdBy, + creationSource: "mcp", + text: "Run every check", + attachments: [], + }, + id, + ordinal, + ); + it.effect.each([ + ["an agent-started turn", message("turn_start", "agent"), ["3"]], + [ + "a notification", + boundary({ + type: "notification", + source: { kind: "delegated_task", taskIds: [] }, + outcome: "completed", + summary: "Checks finished", + }), + ["3"], + ], + ["a steer, paging on to its turn", message("steer", "user"), ["3", "2", "1"]], + ] as const)("stops paging earlier history at %s", ([, item, expected]) => + Effect.gen(function* () { + const pages: Record> = { + "3": [item], + "2": [command("work-1", "completed", 0, 1)], + "1": [message("turn_start", "user", "opener", 0)], + }; + const requests: string[] = []; + const harness = yield* makeHarness({ + cached: cachedItem(command("tail", "completed", 0, 3)), + cachedHistory: { historyCursor: "3", hasMoreHistory: true }, + historyHttpClient: HttpClient.make((request, url) => { + const cursor = url.searchParams.get("cursor")!; + requests.push(cursor); + const items = (pages[cursor] ?? []) as ReadonlyArray; + return Effect.succeed( + historyPage(request, items.map(localRow), String(Number(cursor) - 1)), + ); + }), + }); + expect(yield* harness.loadEarlier()).toEqual({ _tag: "loaded" }); + expect(requests).toEqual(expected); + }), + ); it.effect("loads deferred work on demand and resumes a failed page", () => Effect.gen(function* () { const anchor = { diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index 3e8f3709a915..b67245e735cc 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -75,6 +75,17 @@ function formatHistoryError(error: unknown): string { return "Could not load earlier activity."; } +// Steers join a running turn; every other user message opens one. The timeline shows every +// notification as a response boundary, whether it opened its run or arrived mid-run. +function startsTurn(item: OrchestrationV2TurnItem): boolean { + return ( + item.type === "notification" || + (item.type === "user_message" && + item.inputIntent !== "steer" && + item.inputIntent !== "promoted_queued_to_steer") + ); +} + function historyMetaFromCachedSnapshot( snapshot: OrchestrationV2ThreadDetailSnapshot, ): ThreadHistoryMeta { @@ -777,9 +788,7 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make ? page.nextCursor !== null : throughEntryId === undefined && page.hasMoreHistory && - !page.items.some( - ({ item }) => item.type === "user_message" && item.createdBy === "user", - )); + !page.items.some(({ item }) => startsTurn(item))); if (keepPaging && (page.nextCursor === null || page.nextCursor === requestCursor)) return yield* failLoad( workAnchorId === undefined