From 68e722ec2e02d933c78bf2b9c8ee40a642206b29 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:16:30 +0200 Subject: [PATCH] fix(app): request a chat outline only for a timeline the daemon has served (#5602) A new agent's draft pane rendered the chat outline with its local tab or draft id, so the web app asked the daemon for that id's prompt index before the agent existed and the daemon logged Agent not found. The outline now waits until the viewed timeline has an epoch, which only a daemon-served agent timeline has. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/e2e/browser/chat-outline.spec.ts | 20 +++++++++++++++ .../app/e2e/support/helpers/chat-outline.ts | 20 +++++++++++++++ packages/app/e2e/support/helpers/creation.ts | 6 +++++ .../agent-stream/chat-outline/model.test.ts | 1 - .../src/agent-stream/chat-outline/model.ts | 7 ++---- .../chat-outline/use-chat-outline.test.tsx | 25 +++++++++++++++++++ .../chat-outline/use-chat-outline.ts | 4 ++- 7 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/app/e2e/browser/chat-outline.spec.ts b/packages/app/e2e/browser/chat-outline.spec.ts index bc58e46ab5..9dd3c33313 100644 --- a/packages/app/e2e/browser/chat-outline.spec.ts +++ b/packages/app/e2e/browser/chat-outline.spec.ts @@ -17,12 +17,14 @@ import { hoverChatOutlinePrompt, chatOutlineRail, movePointerOffChatOutline, + observePromptIndexRequests, pointAtChatOutlineRowEdge, pressEnterOnFocusedPrompt, splitCurrentPanelRight, withStreamingMarkdownOutline, expectReadingStreamedMarkdown, } from "../support/helpers/chat-outline"; +import { createCreationScenario } from "../support/helpers/creation"; import { expectTimelineAtMaximumScrollWithPromptVisible, expectTimelinePromptNotMounted, @@ -266,3 +268,21 @@ test.describe("desktop chat outline", () => { } }); }); + +test("indexes a new agent's prompts only once the agent exists", async ({ page }) => { + const promptIndex = await observePromptIndexRequests(page); + const creation = await createCreationScenario(page); + try { + await page.setViewportSize(WIDE_VIEWPORT); + await creation.openAgentDraft(); + await creation.submitPrompt("Start an agent with an outline."); + await creation.expectPromptVisible(); + + const agentIds = await creation.agentIds(); + expect(agentIds).toHaveLength(1); + await promptIndex.waitForRequestFor(agentIds[0]!); + expect(promptIndex.requestedAgentIds()).toEqual(agentIds); + } finally { + await creation.cleanup(); + } +}); diff --git a/packages/app/e2e/support/helpers/chat-outline.ts b/packages/app/e2e/support/helpers/chat-outline.ts index 09e28cd3af..d219979fcf 100644 --- a/packages/app/e2e/support/helpers/chat-outline.ts +++ b/packages/app/e2e/support/helpers/chat-outline.ts @@ -3,6 +3,7 @@ import { openSettings } from "./app"; import { openSettingsSection } from "./settings"; import { runWorkspaceActionFromCommandCenter } from "./command-center-workspace-actions"; import { seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent"; +import { loadSessionMessageReaders } from "./new-workspace"; export async function withStreamingMarkdownOutline( run: (agent: MockAgentWorkspace) => Promise, @@ -229,3 +230,22 @@ async function requireBoundingBox( } return box; } + +export async function observePromptIndexRequests(page: Page) { + const frames = await loadSessionMessageReaders(); + const agentIds: string[] = []; + page.on("websocket", (socket) => { + socket.on("framesent", ({ payload }) => { + const request = frames.client(payload); + if (request?.type === "agent.timeline.list_prompts.request") { + agentIds.push(request.agentId); + } + }); + }); + return { + async waitForRequestFor(agentId: string) { + await expect.poll(() => agentIds).toContain(agentId); + }, + requestedAgentIds: () => [...new Set(agentIds)], + }; +} diff --git a/packages/app/e2e/support/helpers/creation.ts b/packages/app/e2e/support/helpers/creation.ts index df0ec30066..09cd2ea290 100644 --- a/packages/app/e2e/support/helpers/creation.ts +++ b/packages/app/e2e/support/helpers/creation.ts @@ -206,6 +206,12 @@ export async function createCreationScenario(page: Page) { ) .toBe(count); }, + async agentIds() { + await requests.settled(); + return (await project.client.fetchAgents()).entries + .filter(({ agent }) => agent.workspaceId === workspaceId) + .map(({ agent }) => agent.id); + }, async expectAgentTitle(title: string) { await expect .poll( diff --git a/packages/app/src/agent-stream/chat-outline/model.test.ts b/packages/app/src/agent-stream/chat-outline/model.test.ts index 81811c7d78..f11be1ea77 100644 --- a/packages/app/src/agent-stream/chat-outline/model.test.ts +++ b/packages/app/src/agent-stream/chat-outline/model.test.ts @@ -12,7 +12,6 @@ describe("chat outline prompt index epoch", () => { it("accepts only the authoritative timeline epoch", () => { expect(shouldAcceptPromptIndexEpoch("epoch-2", "epoch-2")).toBe(true); expect(shouldAcceptPromptIndexEpoch("epoch-2", "epoch-1")).toBe(false); - expect(shouldAcceptPromptIndexEpoch(null, "epoch-1")).toBe(true); }); }); diff --git a/packages/app/src/agent-stream/chat-outline/model.ts b/packages/app/src/agent-stream/chat-outline/model.ts index 3d71d52b5f..0fb4b5dcb6 100644 --- a/packages/app/src/agent-stream/chat-outline/model.ts +++ b/packages/app/src/agent-stream/chat-outline/model.ts @@ -2,11 +2,8 @@ import type { AgentTimelinePromptIndexPayload } from "@getpaseo/client/internal/ export type ChatOutlinePrompt = AgentTimelinePromptIndexPayload["prompts"][number]; -export function shouldAcceptPromptIndexEpoch( - timelineEpoch: string | null, - indexEpoch: string, -): boolean { - return timelineEpoch === null || timelineEpoch === indexEpoch; +export function shouldAcceptPromptIndexEpoch(timelineEpoch: string, indexEpoch: string): boolean { + return timelineEpoch === indexEpoch; } /** diff --git a/packages/app/src/agent-stream/chat-outline/use-chat-outline.test.tsx b/packages/app/src/agent-stream/chat-outline/use-chat-outline.test.tsx index d84db755e9..4b2139fef6 100644 --- a/packages/app/src/agent-stream/chat-outline/use-chat-outline.test.tsx +++ b/packages/app/src/agent-stream/chat-outline/use-chat-outline.test.tsx @@ -37,6 +37,31 @@ describe("useChatOutline", () => { runtime.subscribeAgentTimeline.mockClear(); }); + it("waits for a served timeline before requesting its prompt index", async () => { + runtime.listAgentTimelinePrompts.mockResolvedValue({ epoch: "epoch-1", prompts: [] }); + const viewportRef = createRef(); + const { rerender } = renderHook( + ({ agentId, timelineEpoch }) => + useChatOutline({ + agentId, + serverId: "server-1", + timelineEpoch, + tail: [], + head: [], + enabled: true, + viewportRef, + onJumpError: vi.fn(), + }), + { initialProps: { agentId: "draft_msg_1", timelineEpoch: null as string | null } }, + ); + + expect(runtime.listAgentTimelinePrompts).not.toHaveBeenCalled(); + + rerender({ agentId: "agent-1", timelineEpoch: "epoch-1" }); + await waitFor(() => expect(runtime.listAgentTimelinePrompts).toHaveBeenCalledTimes(1)); + expect(runtime.listAgentTimelinePrompts).toHaveBeenCalledWith("agent-1"); + }); + it("drops a late prompt index after the authoritative timeline epoch changes", async () => { const first = deferred<{ epoch: string; prompts: [] }>(); const second = deferred<{ diff --git a/packages/app/src/agent-stream/chat-outline/use-chat-outline.ts b/packages/app/src/agent-stream/chat-outline/use-chat-outline.ts index b59e670a5d..015b883d21 100644 --- a/packages/app/src/agent-stream/chat-outline/use-chat-outline.ts +++ b/packages/app/src/agent-stream/chat-outline/use-chat-outline.ts @@ -77,8 +77,10 @@ export function useChatOutline({ useEffect(() => setIndex(null), [agentId, enabled, serverId, timelineEpoch]); + // Only a timeline the daemon has served can be indexed. A draft's optimistic stream has no + // epoch, and its id names no agent the daemon knows. useEffect(() => { - if (!isWeb || !enabled) { + if (!isWeb || !enabled || timelineEpoch === null) { setIndex(null); return; }