diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index 56815ce..77e42f8 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -94,4 +94,100 @@ describe("CustomAgentChatView", () => { expect.any(Function), ); }); + + it("falls back to a fresh launch when reattach reports NOT_FOUND", async () => { + const agent = { + launchAgentChat: vi.fn(async () => ({ + sessionId: "chat-session-2", + assignedConversationId: "conversation-2", + })), + reattachAgentChat: vi + .fn() + .mockRejectedValueOnce({ code: "NOT_FOUND", message: "structured session gone" }) + .mockImplementationOnce(async (sessionId: string) => ({ + sessionId, + scrollback: [], + })), + sendAgentChat: vi.fn(() => new Promise(() => {})), + cancelAgentChat: vi.fn(async () => {}), + closeAgentChat: vi.fn(async () => {}), + }; + const onSessionId = vi.fn(); + + render( + null) }, + } as unknown as Gateways} + > + + , + ); + + await waitFor(() => + expect(agent.launchAgentChat).toHaveBeenCalledWith("project-1", "agent-1", { + cwd: "/repo", + rows: 24, + cols: 80, + conversationId: "conversation-1", + nodeId: "node-1", + }), + ); + await waitFor(() => + expect(onSessionId).toHaveBeenCalledWith("chat-session-2"), + ); + expect(screen.queryByText(/structured session gone/)).toBeNull(); + }); + + it("surfaces non-NOT_FOUND reattach errors instead of launching a new session", async () => { + const agent = { + launchAgentChat: vi.fn(), + reattachAgentChat: vi.fn().mockRejectedValueOnce({ + code: "PROCESS", + message: "structured session failed to attach", + }), + sendAgentChat: vi.fn(() => new Promise(() => {})), + cancelAgentChat: vi.fn(async () => {}), + closeAgentChat: vi.fn(async () => {}), + }; + + render( + null) }, + } as unknown as Gateways} + > + + , + ); + + expect((await screen.findByRole("alert")).textContent).toContain( + "structured session failed to attach", + ); + expect(agent.launchAgentChat).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index 7d79681..ad8e60e 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -154,11 +154,18 @@ export function CustomAgentChatView({ setError(null); try { if (sessionId) { - const reattached = await agent.reattachAgentChat!(sessionId, receive); - if (cancelled) return; - setCurrentSession(reattached.sessionId); - setTurns(reattached.scrollback.reduce(foldChunk, [] as ChatTurn[])); - return; + try { + const reattached = await agent.reattachAgentChat!(sessionId, receive); + if (cancelled) return; + setCurrentSession(reattached.sessionId); + setTurns(reattached.scrollback.reduce(foldChunk, [] as ChatTurn[])); + return; + } catch (e) { + // The backend contract for `reattach_agent_chat` (NOT_FOUND) is that + // the session is gone (closed/never live) and the caller falls back + // to a fresh launch — any other error still surfaces to the user. + if ((e as GatewayError)?.code !== "NOT_FOUND") throw e; + } } const launched = await agent.launchAgentChat!(projectId, agentId, { cwd,