From 45cf233abc889ca51e06c529af4a66a685ca0031 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Fri, 31 Jul 2026 12:11:13 +0200 Subject: [PATCH 1/2] fix(pi): clear retry status when streaming resumes --- .../translatePiConversation.test.ts | 24 +++++++++-- .../conversation/translatePiConversation.ts | 35 ++++++++++++---- .../StatusNotificationView.test.ts | 30 +++++++++++++- .../session-update/StatusNotificationView.tsx | 40 ++++++++++++++----- 4 files changed, 106 insertions(+), 23 deletions(-) diff --git a/packages/agent/src/pi/conversation/translatePiConversation.test.ts b/packages/agent/src/pi/conversation/translatePiConversation.test.ts index 4db19cb58e..f0f3e4d6b5 100644 --- a/packages/agent/src/pi/conversation/translatePiConversation.test.ts +++ b/packages/agent/src/pi/conversation/translatePiConversation.test.ts @@ -162,11 +162,17 @@ describe("createPiConversationTranslator", () => { delayMs: 1000, }, ]); + const retriedMessage = assistant([{ type: "text", text: "Done" }]); expect( translator.translateEvent({ - type: "auto_retry_end", - success: true, - attempt: 1, + type: "message_update", + message: retriedMessage, + assistantMessageEvent: { + type: "text_delta", + contentIndex: 0, + delta: "Done", + partial: retriedMessage, + }, }), ).toEqual([ { @@ -175,7 +181,19 @@ describe("createPiConversationTranslator", () => { status: "retrying", isComplete: true, }, + { + type: "assistant_message_chunk", + timestamp: 10, + content: { type: "text", text: "Done" }, + }, ]); + expect( + translator.translateEvent({ + type: "auto_retry_end", + success: true, + attempt: 1, + }), + ).toEqual([]); }); it("renders terminal Pi runtime errors inline", () => { diff --git a/packages/agent/src/pi/conversation/translatePiConversation.ts b/packages/agent/src/pi/conversation/translatePiConversation.ts index 745145623d..ad025153a0 100644 --- a/packages/agent/src/pi/conversation/translatePiConversation.ts +++ b/packages/agent/src/pi/conversation/translatePiConversation.ts @@ -115,7 +115,25 @@ export function createPiConversationTranslator(): PiConversationTranslator { let latestRuntimeTimestamp = 0; let latestConversationTimestamp = 0; let pendingRuntimeError: AgentConversationEvent | undefined; + let retrying = false; let directBashSequence = 0; + + function completeRetry(timestamp: number): AgentConversationEvent[] { + if (!retrying) { + return []; + } + + retrying = false; + return [ + { + type: "runtime_status", + timestamp, + status: "retrying", + isComplete: true, + }, + ]; + } + let activeDirectBash: | { nextOutputBytes: number; @@ -257,6 +275,7 @@ export function createPiConversationTranslator(): PiConversationTranslator { if (update.type === "text_delta" && update.delta) { streamedAssistantTimestamps.add(event.message.timestamp); return [ + ...completeRetry(event.message.timestamp), { type: "assistant_message_chunk", timestamp: event.message.timestamp, @@ -268,6 +287,7 @@ export function createPiConversationTranslator(): PiConversationTranslator { if (update.type === "thinking_delta" && update.delta) { streamedAssistantTimestamps.add(event.message.timestamp); return [ + ...completeRetry(event.message.timestamp), { type: "assistant_thought_chunk", timestamp: event.message.timestamp, @@ -407,7 +427,11 @@ export function createPiConversationTranslator(): PiConversationTranslator { } if (event.type === "auto_retry_start") { + const completedEvents = completeRetry(latestConversationTimestamp); + retrying = true; + return [ + ...completedEvents, { type: "runtime_status", timestamp: latestConversationTimestamp, @@ -421,14 +445,9 @@ export function createPiConversationTranslator(): PiConversationTranslator { } if (event.type === "auto_retry_end") { - const events: AgentConversationEvent[] = [ - { - type: "runtime_status", - timestamp: latestConversationTimestamp, - status: "retrying", - isComplete: true, - }, - ]; + const events: AgentConversationEvent[] = completeRetry( + latestConversationTimestamp, + ); if (!event.success && event.finalError) { events.push({ diff --git a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts index d3be297220..9f7419f4c9 100644 --- a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts +++ b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { formatCompactionFailure } from "./StatusNotificationView"; +import { + formatCompactionFailure, + formatRetryStatus, +} from "./StatusNotificationView"; describe("formatCompactionFailure", () => { it.each([ @@ -16,3 +19,28 @@ describe("formatCompactionFailure", () => { expect(formatCompactionFailure(error)).toBe(expected); }); }); + +describe("formatRetryStatus", () => { + it.each([ + { + input: { + attempt: 1, + maxAttempts: 3, + message: "Rate limit reached for gpt-5.6-terra on token ...", + remainingMs: 0, + }, + expected: "Rate limit reached. Retrying now (attempt 1 of 3)", + }, + { + input: { + attempt: 2, + maxAttempts: 3, + message: "Server overloaded", + remainingMs: 2_000, + }, + expected: "Retrying in 2s (attempt 2 of 3)", + }, + ])("renders a concise retry message", ({ input, expected }) => { + expect(formatRetryStatus(input)).toBe(expected); + }); +}); diff --git a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx index bb468e5794..5ec5c873d3 100644 --- a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx @@ -35,6 +35,29 @@ export function formatCompactionFailure(error?: string): string { return detail ? `Compacting failed: ${detail}` : "Compacting failed"; } +export function formatRetryStatus({ + attempt, + maxAttempts, + message, + remainingMs, +}: { + attempt?: number; + maxAttempts?: number; + message?: string; + remainingMs: number; +}): string { + const rateLimited = /\b(429|rate limit|too many requests)\b/i.test( + message ?? "", + ); + const retryAt = + remainingMs > 0 ? `in ${formatDuration(remainingMs, 0)}` : "now"; + const attemptLabel = + attempt && maxAttempts ? ` (attempt ${attempt} of ${maxAttempts})` : ""; + const prefix = rateLimited ? "Rate limit reached. " : ""; + + return `${prefix}Retrying ${retryAt}${attemptLabel}`; +} + export function StatusNotificationView({ status, isComplete, @@ -179,14 +202,12 @@ function RetryingStatusView({ return () => clearInterval(interval); }, [delayMs, startedAt]); - const attemptLabel = - attempt && maxAttempts - ? `Attempt ${attempt} of ${maxAttempts}` - : "Retrying"; - const retryLabel = - remainingMs > 0 - ? `${attemptLabel} in ${formatDuration(remainingMs, 1)}` - : `${attemptLabel} now`; + const retryLabel = formatRetryStatus({ + attempt, + maxAttempts, + message, + remainingMs, + }); return ( @@ -194,9 +215,6 @@ function RetryingStatusView({ {retryLabel} - {message && ( - {message} - )} From de9badb565908d406a1ab06cbc431a2cced4f969 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Fri, 31 Jul 2026 12:15:16 +0200 Subject: [PATCH 2/2] fix(ui): recognize Pi rate-limit variants --- .../session-update/StatusNotificationView.test.ts | 9 +++++++++ .../components/session-update/StatusNotificationView.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts index 9f7419f4c9..9ed69e072a 100644 --- a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts +++ b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.test.ts @@ -31,6 +31,15 @@ describe("formatRetryStatus", () => { }, expected: "Rate limit reached. Retrying now (attempt 1 of 3)", }, + { + input: { + attempt: 2, + maxAttempts: 3, + message: "Rate limited", + remainingMs: 2_000, + }, + expected: "Rate limit reached. Retrying in 2s (attempt 2 of 3)", + }, { input: { attempt: 2, diff --git a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx index 5ec5c873d3..e1e3b15b76 100644 --- a/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/StatusNotificationView.tsx @@ -46,7 +46,7 @@ export function formatRetryStatus({ message?: string; remainingMs: number; }): string { - const rateLimited = /\b(429|rate limit|too many requests)\b/i.test( + const rateLimited = /\b429\b|rate[ _]limit(?:ed)?|too many requests/i.test( message ?? "", ); const retryAt =