diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 496e283a4..1eb92520f 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -152,4 +152,5 @@ gh pr view --json mergeable,mergeStateStatus | jq '.' ## Mode: Plan +- When Plan Mode is requested, assume the user wants the actual completed plan; do not merely describe how you would devise one. - Attach a net LoC estimate (product code only) to each recommended approach. diff --git a/src/browser/utils/messages/StreamingMessageAggregator.ts b/src/browser/utils/messages/StreamingMessageAggregator.ts index fcdd02d91..e0d1193e1 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.ts @@ -96,6 +96,8 @@ export class StreamingMessageAggregator { // Track when we're waiting for stream-start after user message // Prevents retry barrier flash during normal send flow // Stores timestamp of when user message was sent (null = no pending stream) + // IMPORTANT: We intentionally keep this timestamp until a stream actually starts + // (or the user retries) so retry UI/backoff logic doesn't misfire on send failures. private pendingStreamStartTime: number | null = null; // Workspace creation timestamp (used for recency calculation) diff --git a/src/browser/utils/messages/retryEligibility.test.ts b/src/browser/utils/messages/retryEligibility.test.ts index fb8d1be51..403f1488a 100644 --- a/src/browser/utils/messages/retryEligibility.test.ts +++ b/src/browser/utils/messages/retryEligibility.test.ts @@ -3,6 +3,7 @@ import { hasInterruptedStream, isEligibleForAutoRetry, isNonRetryableSendError, + PENDING_STREAM_START_GRACE_PERIOD_MS, } from "./retryEligibility"; import type { DisplayedMessage } from "@/common/types/message"; import type { SendMessageError } from "@/common/types/errors"; @@ -165,7 +166,7 @@ describe("hasInterruptedStream", () => { expect(hasInterruptedStream(messages, null)).toBe(true); }); - it("returns false when message was sent very recently (< 3s)", () => { + it("returns false when message was sent very recently (within grace period)", () => { const messages: DisplayedMessage[] = [ { type: "user", @@ -194,8 +195,8 @@ describe("hasInterruptedStream", () => { historySequence: 3, }, ]; - // Message sent 1 second ago - still within 3s window - const recentTimestamp = Date.now() - 1000; + // Message sent 1 second ago - still within grace window + const recentTimestamp = Date.now() - (PENDING_STREAM_START_GRACE_PERIOD_MS - 1000); expect(hasInterruptedStream(messages, recentTimestamp)).toBe(false); }); @@ -212,7 +213,7 @@ describe("hasInterruptedStream", () => { expect(hasInterruptedStream(messages, null)).toBe(true); }); - it("returns false when user message just sent (< 3s ago)", () => { + it("returns false when user message just sent (within grace period)", () => { const messages: DisplayedMessage[] = [ { type: "user", @@ -222,11 +223,11 @@ describe("hasInterruptedStream", () => { historySequence: 1, }, ]; - const justSent = Date.now() - 500; // 0.5s ago + const justSent = Date.now() - (PENDING_STREAM_START_GRACE_PERIOD_MS - 500); expect(hasInterruptedStream(messages, justSent)).toBe(false); }); - it("returns true when message sent over 3s ago (stream likely hung)", () => { + it("returns true when message sent beyond grace period (stream likely hung)", () => { const messages: DisplayedMessage[] = [ { type: "user", @@ -236,7 +237,7 @@ describe("hasInterruptedStream", () => { historySequence: 1, }, ]; - const longAgo = Date.now() - 4000; // 4s ago - past 3s threshold + const longAgo = Date.now() - (PENDING_STREAM_START_GRACE_PERIOD_MS + 1000); expect(hasInterruptedStream(messages, longAgo)).toBe(true); }); @@ -545,7 +546,7 @@ describe("isEligibleForAutoRetry", () => { expect(isEligibleForAutoRetry(messages, null)).toBe(true); }); - it("returns false when user message sent very recently (< 3s)", () => { + it("returns false when user message sent very recently (within grace period)", () => { const messages: DisplayedMessage[] = [ { type: "user", @@ -555,7 +556,7 @@ describe("isEligibleForAutoRetry", () => { historySequence: 1, }, ]; - const justSent = Date.now() - 500; // 0.5s ago + const justSent = Date.now() - (PENDING_STREAM_START_GRACE_PERIOD_MS - 500); expect(isEligibleForAutoRetry(messages, justSent)).toBe(false); }); }); diff --git a/src/browser/utils/messages/retryEligibility.ts b/src/browser/utils/messages/retryEligibility.ts index 3d5f6fff2..eed8ec69f 100644 --- a/src/browser/utils/messages/retryEligibility.ts +++ b/src/browser/utils/messages/retryEligibility.ts @@ -18,6 +18,8 @@ declare global { } } +export const PENDING_STREAM_START_GRACE_PERIOD_MS = 15000; // 15 seconds + /** * Check if the debug flag to force all errors to be retryable is enabled */ @@ -69,7 +71,7 @@ export function isNonRetryableSendError(error: SendMessageError): boolean { * 3. Last message is a user message (indicating we sent it but never got a response) * - This handles app restarts during slow model responses (models can take 30-60s to first token) * - User messages are only at the end when response hasn't started/completed - * - EXCEPT: Not if recently sent (<3s ago) - prevents flash during normal send flow + * - EXCEPT: Not if recently sent (within PENDING_STREAM_START_GRACE_PERIOD_MS) - prevents flash during normal send flow */ export function hasInterruptedStream( messages: DisplayedMessage[], @@ -77,12 +79,12 @@ export function hasInterruptedStream( ): boolean { if (messages.length === 0) return false; - // Don't show retry barrier if user message was sent very recently (< 3s) + // Don't show retry barrier if user message was sent very recently (within the grace period) // This prevents flash during normal send flow while stream-start event arrives - // After 3s, we assume something is wrong and show the barrier + // After the grace period, assume something is wrong and show the barrier if (pendingStreamStartTime !== null) { const elapsed = Date.now() - pendingStreamStartTime; - if (elapsed < 3000) return false; + if (elapsed < PENDING_STREAM_START_GRACE_PERIOD_MS) return false; } const lastMessage = messages[messages.length - 1];