Skip to content

ECHO-428 Portal "Conversation Ended" fixed#272

Merged
ussaama merged 5 commits intomainfrom
conversation-ended-error-handling
Aug 28, 2025
Merged

ECHO-428 Portal "Conversation Ended" fixed#272
ussaama merged 5 commits intomainfrom
conversation-ended-error-handling

Conversation

@ussaama
Copy link
Copy Markdown
Contributor

@ussaama ussaama commented Aug 28, 2025

Summary by CodeRabbit

  • Bug Fixes
    • Recording now stops automatically when a conversation is deleted or access is revoked, preventing stuck or misleading states.
    • Improved error handling to distinguish permanent errors (stop recording) from transient ones (continue recording).
    • Reduced unnecessary retries for permanently missing/unavailable conversations; transient errors retry up to a limited number.
    • Increased retry attempts for upload operations to improve resilience during flaky networks.

- added retry logic to check conversation endpoint
@linear
Copy link
Copy Markdown

linear bot commented Aug 28, 2025

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Aug 28, 2025

Walkthrough

Adds AxiosError import and retry-policy changes in participant hooks; increases chunk upload retries; introduces status-aware retry logic for conversation queries; reformats hook signatures; and updates ParticipantConversation to detect specific HTTP deletion statuses (403/404/410), stop recording, and set conversationDeletedDuringRecording during recording.

Changes

Cohort / File(s) Summary
Participant hooks: retry policy, upload retries & signature formatting
echo/frontend/src/components/participant/hooks/index.ts
Import AxiosError; increase useUploadConversationChunk retry attempts from 10 to 20; add status-aware retry for useConversationQuery (no retry for 404/403/410, otherwise up to 6 retries); reformat useConversationRepliesQuery and useConversationQuery signatures to multiline and apply minor formatting tweaks.
Participant conversation: deletion detection during recording
echo/frontend/src/routes/participant/ParticipantConversation.tsx
Add CONVERSATION_DELETION_STATUS_CODES = [404, 403, 410]; update recording-status effect to early-return when not recording; when fetch error occurs and status is in deletion codes, log, call stopRecording, and set conversationDeletedDuringRecording; keep recording for other transient errors; update effect dependencies (isLoading, isFetching, error, isError, stopRecording).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

Assessment against linked issues

Objective Addressed Explanation
Mitigate/handle “Conversation Deleted” incidents during ongoing meetings [ECHO-428]

Possibly related PRs

LGTM.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch conversation-ended-error-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ussaama ussaama requested a review from spashii August 28, 2025 09:57
@coderabbitai coderabbitai bot added bug Something isn't working improvement labels Aug 28, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between afd78bf and ebd7509.

📒 Files selected for processing (2)
  • echo/frontend/src/components/participant/hooks/index.ts (4 hunks)
  • echo/frontend/src/routes/participant/ParticipantConversation.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses regular useQuery (not useSuspenseQuery), which means conversationsCountQuery.data can be undefined during loading states. When using Number(conversationsCountQuery.data) ?? 0, this creates NaN because Number(undefined) = NaN and NaN is not nullish, so the fallback doesn't apply. The correct pattern is Number(conversationsCountQuery.data ?? 0) to ensure the fallback happens before type conversion.

Applied to files:

  • echo/frontend/src/components/participant/hooks/index.ts
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses useSuspenseQuery with Directus aggregate, which always returns string numbers like "0", "1", "2" and suspends during loading instead of returning undefined. Therefore, Number(conversationsCountQuery.data) ?? 0 is safe and the Number() conversion is necessary for type conversion from string to number.

Applied to files:

  • echo/frontend/src/components/participant/hooks/index.ts
🧬 Code graph analysis (1)
echo/frontend/src/components/participant/hooks/index.ts (1)
echo/frontend/src/lib/api.ts (1)
  • getParticipantConversationById (40-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci-build-servers (dbr-echo-server, ./echo/server, Dockerfile, dbr-echo-server)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (4)
echo/frontend/src/routes/participant/ParticipantConversation.tsx (1)

60-60: LGTM: AxiosError import is correct and scoped to the new effect.

echo/frontend/src/components/participant/hooks/index.ts (3)

16-16: LGTM: AxiosError import aligns with retry predicate typing.


311-314: LGTM: Signature reflow only.


361-361: No-op change detected. LGTM.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
echo/frontend/src/routes/participant/ParticipantConversation.tsx (1)

199-203: Good catch: include conversationQuery.error in deps

Prevents stale handling when the error object changes while flags remain true.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ebd7509 and 14f7f0c.

📒 Files selected for processing (1)
  • echo/frontend/src/routes/participant/ParticipantConversation.tsx (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci-build-servers (dbr-echo-server, ./echo/server, Dockerfile, dbr-echo-server)
  • GitHub Check: ci-build-servers (dbr-echo-directus, ./echo/directus, Dockerfile, dbr-echo-directus)
🔇 Additional comments (3)
echo/frontend/src/routes/participant/ParticipantConversation.tsx (3)

169-175: Solid guardrail: effect short-circuits when not recording and waits for settled query

This prevents spurious state updates and avoids noise during fetches. LGTM.


179-189: Correctly stop recording on 403/404/410 with PII-safe logging

Nice. Stopping immediately and flipping conversationDeletedDuringRecording addresses ECHO-428 without leaking response.data.


190-194: Transient error path is clean and non-disruptive

Continuing to record while warning with sanitized fields is the right UX.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
echo/frontend/src/routes/participant/ParticipantConversation.tsx (1)

175-177: Use axios.isAxiosError to narrow before reading response.
Avoids unsafe property access if the error isn’t an AxiosError (and keeps TS happy).

Apply within this range:

-      const error = conversationQuery.error;
-      const httpStatus = error?.response?.status;
+      const error = conversationQuery.error;
+      const httpStatus = axios.isAxiosError(error)
+        ? error.response?.status
+        : undefined;

Add import (outside this range):

import axios from "axios";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 14f7f0c and ecb449d.

📒 Files selected for processing (1)
  • echo/frontend/src/routes/participant/ParticipantConversation.tsx (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-check-server
🔇 Additional comments (4)
echo/frontend/src/routes/participant/ParticipantConversation.tsx (4)

62-62: Deletion status codes: solid coverage (includes 410). LGTM.
Covers 403/404/410—matches common “gone/forbidden/not found” mid-session cases.


168-174: Effect guardrails are correct.
Early return when not recording and gating on isError while skipping loading/fetching avoids false positives. Clean.


178-188: Stop-on-deletion path is tight.
Sanitized logging, immediate stopRecording, and state flip prevent repeat triggers via the isRecording gate. LGTM.


198-201: Deps list now reacts to new errors.
Including isLoading/isFetching/error prevents stale effect state. Good catch.

@ussaama ussaama enabled auto-merge August 28, 2025 10:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
echo/frontend/src/components/participant/hooks/index.ts (1)

329-349: Type the query for end-to-end safety.

Let React Query carry AxiosError so you don’t have to annotate the retry param.

-  return useQuery({
+  return useQuery<Awaited<ReturnType<typeof getParticipantConversationById>>, AxiosError>({
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecb449d and aaab4c8.

📒 Files selected for processing (1)
  • echo/frontend/src/components/participant/hooks/index.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses regular useQuery (not useSuspenseQuery), which means conversationsCountQuery.data can be undefined during loading states. When using Number(conversationsCountQuery.data) ?? 0, this creates NaN because Number(undefined) = NaN and NaN is not nullish, so the fallback doesn't apply. The correct pattern is Number(conversationsCountQuery.data ?? 0) to ensure the fallback happens before type conversion.

Applied to files:

  • echo/frontend/src/components/participant/hooks/index.ts
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses useSuspenseQuery with Directus aggregate, which always returns string numbers like "0", "1", "2" and suspends during loading instead of returning undefined. Therefore, Number(conversationsCountQuery.data) ?? 0 is safe and the Number() conversion is necessary for type conversion from string to number.

Applied to files:

  • echo/frontend/src/components/participant/hooks/index.ts
🧬 Code graph analysis (1)
echo/frontend/src/components/participant/hooks/index.ts (1)
echo/frontend/src/lib/api.ts (1)
  • getParticipantConversationById (40-44)
🔇 Additional comments (2)
echo/frontend/src/components/participant/hooks/index.ts (2)

311-313: Signature cleanup LGTM.

Multiline + string | undefined with enabled: !!conversationId is solid.


361-362: LGTM.

Trailing brace/newline churn only.

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "@/components/common/Toaster";
import { useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Import is fine; inline type on retry can be avoided.

You can drop the inline AxiosError param typing by typing the query generics instead (see suggestion below). Keeps the import, but avoids repeating the type in function params.

🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/hooks/index.ts around line 16, the
review notes that the inline AxiosError parameter typing can be removed by
supplying the appropriate generics to the query/mutation call instead; keep the
existing AxiosError import, remove the explicit AxiosError type annotation from
the callback parameter, and set the query/mutation generic type parameters
(e.g., <Data, AxiosError, Variables>) so the error type is inferred without
repeating it in the function signature.

Comment on lines +335 to +347
queryFn: () =>
getParticipantConversationById(projectId ?? "", conversationId ?? ""),
enabled: !!conversationId && !!projectId,
refetchInterval: 60000,
retry: (failureCount, error: AxiosError) => {
const status = error?.response?.status;
// Don't retry if conversation is deleted
if (status && [404, 403, 410].includes(status as number)) {
return false;
}

return failureCount < 6;
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Narrow status without casts; kill the as number.

Safer type guard; no functional change.

-    retry: (failureCount, error: AxiosError) => {
-      const status = error?.response?.status;
-      // Don't retry if conversation is deleted
-      if (status && [404, 403, 410].includes(status as number)) {
+    retry: (failureCount, error: AxiosError) => {
+      const status = error?.response?.status;
+      // Don't retry if conversation is deleted/forbidden/gone
+      if (typeof status === "number" && [404, 403, 410].includes(status)) {
         return false;
       }
-
       return failureCount < 6;
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
queryFn: () =>
getParticipantConversationById(projectId ?? "", conversationId ?? ""),
enabled: !!conversationId && !!projectId,
refetchInterval: 60000,
retry: (failureCount, error: AxiosError) => {
const status = error?.response?.status;
// Don't retry if conversation is deleted
if (status && [404, 403, 410].includes(status as number)) {
return false;
}
return failureCount < 6;
},
queryFn: () =>
getParticipantConversationById(projectId ?? "", conversationId ?? ""),
enabled: !!conversationId && !!projectId,
refetchInterval: 60000,
retry: (failureCount, error: AxiosError) => {
const status = error?.response?.status;
// Don't retry if conversation is deleted/forbidden/gone
if (typeof status === "number" && [404, 403, 410].includes(status)) {
return false;
}
return failureCount < 6;
},
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/hooks/index.ts around lines 335 to
347, the retry handler narrows the AxiosError response status using an
unnecessary type cast "as number"; replace that cast with a proper type guard so
you only check numeric statuses (e.g. ensure status is a number before checking
inclusion in [404, 403, 410]). Update the conditional to first verify typeof
status === "number" (or use Number.isInteger/status != null && typeof status ===
"number") and then test the array membership, removing the "as number" cast.

Comment on lines 338 to +347
refetchInterval: 60000,
retry: (failureCount, error: AxiosError) => {
const status = error?.response?.status;
// Don't retry if conversation is deleted
if (status && [404, 403, 410].includes(status as number)) {
return false;
}

return failureCount < 6;
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Stop polling after 404/403/410 to avoid minute-by-minute error pings.

Disable refetchInterval when the last error is a deletion code.

-    refetchInterval: 60000,
+    refetchInterval: (_data, query) => {
+      const err = query.state.error;
+      const status =
+        (typeof (err as any)?.response?.status === "number"
+          ? (err as any).response.status
+          : undefined);
+      return typeof status === "number" && [404, 403, 410].includes(status) ? false : 60000;
+    },

If you prefer a type-safe guard, import isAxiosError:

- import { AxiosError } from "axios";
+ import { AxiosError, isAxiosError } from "axios";

And then:

-      const status =
-        (typeof (err as any)?.response?.status === "number"
-          ? (err as any).response.status
-          : undefined);
+      const status = isAxiosError(err) ? err.response?.status : undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
refetchInterval: 60000,
retry: (failureCount, error: AxiosError) => {
const status = error?.response?.status;
// Don't retry if conversation is deleted
if (status && [404, 403, 410].includes(status as number)) {
return false;
}
return failureCount < 6;
},
import { AxiosError, isAxiosError } from "axios";
refetchInterval: (_data, query) => {
const err = query.state.error;
const status = isAxiosError(err) ? err.response?.status : undefined;
// Stop polling on deletion/forbidden/gone
return typeof status === "number" && [404, 403, 410].includes(status)
? false
: 60000;
},

Comment on lines +341 to +344
// Don't retry if conversation is deleted
if (status && [404, 403, 410].includes(status as number)) {
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Deduplicate deletion status codes (now used in ≥2 places).

These codes also live in ParticipantConversation.tsx; centralize to prevent drift.

+import { CONVERSATION_DELETION_STATUS_CODES } from "@/constants/http";
@@
-      if (typeof status === "number" && [404, 403, 410].includes(status)) {
+      if (typeof status === "number" && CONVERSATION_DELETION_STATUS_CODES.includes(status)) {
         return false;
       }

Add a tiny constant (zero “architecture” overhead):

// echo/frontend/src/constants/http.ts
export const CONVERSATION_DELETION_STATUS_CODES = [404, 403, 410] as const;

🏁 Script executed:

#!/bin/bash
# Evidence of duplication to justify the constant
rg -nC2 'CONVERSATION_DELETION_STATUS_CODES|\\[(404|403|410)\\]' --type=ts

Length of output: 237318


Centralize conversation deletion HTTP status codes
Extract [404, 403, 410] into echo/frontend/src/constants/http.ts as

export const CONVERSATION_DELETION_STATUS_CODES = [404, 403, 410] as const;

Then in both files…

  • src/components/participant/hooks/index.ts (around lines 341–344)
  • src/routes/participant/ParticipantConversation.tsx (remove its local CONVERSATION_DELETION_STATUS_CODES declaration)
    …replace literal checks with
import { CONVERSATION_DELETION_STATUS_CODES } from "@/constants/http";

if (typeof status === "number" && CONVERSATION_DELETION_STATUS_CODES.includes(status)) {
  return false;
}
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/hooks/index.ts around lines 341–344,
extract the literal array [404, 403, 410] into
echo/frontend/src/constants/http.ts as a named export
CONVERSATION_DELETION_STATUS_CODES, then import that constant here and replace
the current check with a type-safe number check that uses
CONVERSATION_DELETION_STATUS_CODES.includes(status); also remove the local
CONVERSATION_DELETION_STATUS_CODES declaration from
src/routes/participant/ParticipantConversation.tsx and update that file to
import and use the shared constant instead so both places use the centralized
export.

@cursor
Copy link
Copy Markdown

cursor bot commented Aug 28, 2025

🚨 Bugbot Trial Expired

Your team's Bugbot trial has expired. Please contact your team administrator to turn on the paid plan to continue using Bugbot.

A team admin can activate the plan in the Cursor dashboard.

@ussaama ussaama added this pull request to the merge queue Aug 28, 2025
Merged via the queue into main with commit 9691b68 Aug 28, 2025
11 of 12 checks passed
@ussaama ussaama deleted the conversation-ended-error-handling branch August 28, 2025 11:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
echo/frontend/src/components/participant/hooks/index.ts (9)

76-83: Snapshot both caches; current context only captures one, so rollback is partial.

You optimistic-update two keys but only snapshot one. Capture both to enable full rollback.

-      // Snapshot the previous value
-      const previousChunks = queryClient.getQueryData([
-        "conversations",
-        variables.conversationId,
-        "chunks",
-      ]);
+      // Snapshot the previous values for both caches we mutate
+      const previousConversationChunks = queryClient.getQueryData<
+        ConversationChunk[] | undefined
+      >(["conversations", variables.conversationId, "chunks"]);
+      const previousParticipantChunks = queryClient.getQueryData<
+        ConversationChunk[] | undefined
+      >(["participant", "conversation_chunks", variables.conversationId]);
@@
-      // Return a context object with the snapshotted value
-      return { previousChunks };
+      // Return both snapshots for rollback
+      return { previousConversationChunks, previousParticipantChunks };

84-101: Optimistic update drops the new chunk when cache is empty.

If oldData is undefined you return [], losing the optimistic chunk.

-      queryClient.setQueryData(
+      queryClient.setQueryData(
         ["conversations", variables.conversationId, "chunks"],
         (oldData: ConversationChunk[] | undefined) => {
-          return oldData
-            ? [
-                ...oldData,
-                {
-                  id: "optimistic-" + Date.now(),
-                  conversation_id: variables.conversationId,
-                  created_at: new Date().toISOString(),
-                  timestamp: new Date().toISOString(),
-                  updated_at: new Date().toISOString(),
-                  transcript: undefined,
-                } as ConversationChunk,
-              ]
-            : [];
+          const optimistic = {
+            id: "optimistic-" + Date.now(),
+            conversation_id: variables.conversationId,
+            created_at: new Date().toISOString(),
+            timestamp: new Date().toISOString(),
+            updated_at: new Date().toISOString(),
+            transcript: undefined,
+          } as ConversationChunk;
+          return oldData ? [...oldData, optimistic] : [optimistic];
         },
       );

103-121: Same optimistic-update bug on the participant key.

Mirror the fix so both caches are consistent.

-      queryClient.setQueryData(
+      queryClient.setQueryData(
         ["participant", "conversation_chunks", variables.conversationId],
         (oldData: ConversationChunk[] | undefined) => {
-          return oldData
-            ? [
-                ...oldData,
-                {
-                  id: "optimistic-" + Date.now(),
-                  conversation_id: variables.conversationId,
-                  created_at: new Date().toISOString(),
-                  timestamp: new Date().toISOString(),
-                  updated_at: new Date().toISOString(),
-                  transcript: undefined,
-                } as ConversationChunk,
-              ]
-            : [];
+          const optimistic = {
+            id: "optimistic-" + Date.now(),
+            conversation_id: variables.conversationId,
+            created_at: new Date().toISOString(),
+            timestamp: new Date().toISOString(),
+            updated_at: new Date().toISOString(),
+            transcript: undefined,
+          } as ConversationChunk;
+          return oldData ? [...oldData, optimistic] : [optimistic];
         },
       );

127-133: Rollback both caches on error (currently only one is rolled back).

Without restoring the participant key, the optimistic ghost chunk persists there.

-    onError: (_err, variables, context) => {
-      queryClient.setQueryData(
-        ["conversations", variables.conversationId, "chunks"],
-        context?.previousChunks,
-      );
-    },
+    onError: (_err, variables, context) => {
+      queryClient.setQueryData(
+        ["conversations", variables.conversationId, "chunks"],
+        context?.previousConversationChunks,
+      );
+      queryClient.setQueryData(
+        ["participant", "conversation_chunks", variables.conversationId],
+        context?.previousParticipantChunks,
+      );
+    },

150-156: Apply the same status-aware retry + backoff to text chunk uploads.

Keeps behavior consistent and reduces error storms on deleted conversations.

   return useMutation({
     mutationFn: uploadConversationText,
-    retry: 10,
+    retry: (failureCount, error) => {
+      const status = isAxiosError(error) ? error.response?.status : undefined;
+      if (typeof status === "number" && [400, 401, 403, 404, 409, 410, 422].includes(status)) {
+        return false;
+      }
+      return failureCount < 8;
+    },
+    retryDelay: (attempt) =>
+      Math.min(30000, 1000 * 2 ** attempt) + Math.floor(Math.random() * 500),

172-221: Mirror snapshot + optimistic-update fixes for text chunks.

Two fixes: snapshot both caches; don’t return [] on empty cache.

-      // Snapshot the previous value
-      const previousChunks = queryClient.getQueryData([
-        "conversations",
-        variables.conversationId,
-        "chunks",
-      ]);
+      // Snapshot both caches
+      const previousConversationChunks = queryClient.getQueryData<
+        ConversationChunk[] | undefined
+      >(["conversations", variables.conversationId, "chunks"]);
+      const previousParticipantChunks = queryClient.getQueryData<
+        ConversationChunk[] | undefined
+      >(["participant", "conversation_chunks", variables.conversationId]);
@@
-      queryClient.setQueryData(
+      queryClient.setQueryData(
         ["conversations", variables.conversationId, "chunks"],
         (oldData: ConversationChunk[] | undefined) => {
-          return oldData
-            ? [
-                ...oldData,
-                {
-                  id: "optimistic-" + Date.now(),
-                  conversation_id: variables.conversationId,
-                  created_at: new Date().toISOString(),
-                  timestamp: new Date().toISOString(),
-                  updated_at: new Date().toISOString(),
-                  transcript: undefined,
-                } as ConversationChunk,
-              ]
-            : [];
+          const optimistic = {
+            id: "optimistic-" + Date.now(),
+            conversation_id: variables.conversationId,
+            created_at: new Date().toISOString(),
+            timestamp: new Date().toISOString(),
+            updated_at: new Date().toISOString(),
+            transcript: undefined,
+          } as ConversationChunk;
+          return oldData ? [...oldData, optimistic] : [optimistic];
         },
       );
@@
-      // Return a context object with the snapshotted value
-      return { previousChunks };
+      // Return both snapshots
+      return { previousConversationChunks, previousParticipantChunks };

223-229: Rollback both caches on text upload error.

Parity with binary chunk path.

-    onError: (_err, variables, context) => {
-      queryClient.setQueryData(
-        ["conversations", variables.conversationId, "chunks"],
-        context?.previousChunks,
-      );
-    },
+    onError: (_err, variables, context) => {
+      queryClient.setQueryData(
+        ["conversations", variables.conversationId, "chunks"],
+        context?.previousConversationChunks,
+      );
+      queryClient.setQueryData(
+        ["participant", "conversation_chunks", variables.conversationId],
+        context?.previousParticipantChunks,
+      );
+    },

351-361: Do the same “stop polling after deletion” for chunks query.

Prevents background error spam when a conversation is gone.

   return useQuery({
     queryKey: ["participant", "conversation_chunks", conversationId],
     queryFn: () =>
       getParticipantConversationChunks(projectId ?? "", conversationId ?? ""),
-    refetchInterval: 60000,
+    refetchInterval: (_data, query) => {
+      const err = query.state.error;
+      const status = isAxiosError(err) ? err.response?.status : undefined;
+      return typeof status === "number" && [404, 403, 410].includes(status) ? false : 60000;
+    },
   });

134-147: Add conversation chunks invalidation under onSettled
Add

queryClient.invalidateQueries({
  queryKey: ["conversations", variables.conversationId, "chunks"],
});

to the onSettled callback so the same ["conversations", id, "chunks"] queries (used throughout participant/hooks/index.ts) are properly refreshed.

♻️ Duplicate comments (3)
echo/frontend/src/components/participant/hooks/index.ts (3)

16-16: Prefer query generics over inline AxiosError; also import isAxiosError for safe guards.

Inline typing on the retry callback is unnecessary if you type the query. Add isAxiosError for status guards used below.

-import { AxiosError } from "axios";
+import { AxiosError, isAxiosError } from "axios";

335-347: Narrow status without casts; type the query to drop inline AxiosError.

Use a type guard and let generics flow AxiosError into the callback. Same suggestion was previously discussed.

-  return useQuery({
+  return useQuery<Awaited<ReturnType<typeof getParticipantConversationById>>, AxiosError>({
@@
-    retry: (failureCount, error: AxiosError) => {
-      const status = error?.response?.status;
-      // Don't retry if conversation is deleted
-      if (status && [404, 403, 410].includes(status as number)) {
+    retry: (failureCount, error) => {
+      const status = isAxiosError(error) ? error.response?.status : undefined;
+      // Don't retry if conversation is deleted/forbidden/gone
+      if (typeof status === "number" && [404, 403, 410].includes(status)) {
         return false;
       }
 
       return failureCount < 6;
     },

338-343: Stop polling after 404/403/410 to avoid minute-by-minute error pings.

Disable refetchInterval when last error indicates deletion/gone/forbidden.

-    refetchInterval: 60000,
+    refetchInterval: (_data, query) => {
+      const err = query.state.error;
+      const status = isAxiosError(err) ? err.response?.status : undefined;
+      return typeof status === "number" && [404, 403, 410].includes(status) ? false : 60000;
+    },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aaab4c8 and b811722.

📒 Files selected for processing (1)
  • echo/frontend/src/components/participant/hooks/index.ts (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses regular useQuery (not useSuspenseQuery), which means conversationsCountQuery.data can be undefined during loading states. When using Number(conversationsCountQuery.data) ?? 0, this creates NaN because Number(undefined) = NaN and NaN is not nullish, so the fallback doesn't apply. The correct pattern is Number(conversationsCountQuery.data ?? 0) to ensure the fallback happens before type conversion.

Applied to files:

  • echo/frontend/src/components/participant/hooks/index.ts
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses useSuspenseQuery with Directus aggregate, which always returns string numbers like "0", "1", "2" and suspends during loading instead of returning undefined. Therefore, Number(conversationsCountQuery.data) ?? 0 is safe and the Number() conversion is necessary for type conversion from string to number.

Applied to files:

  • echo/frontend/src/components/participant/hooks/index.ts
🧬 Code graph analysis (1)
echo/frontend/src/components/participant/hooks/index.ts (1)
echo/frontend/src/lib/api.ts (1)
  • getParticipantConversationById (40-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci-build-servers (dbr-echo-server, ./echo/server, Dockerfile, dbr-echo-server)
  • GitHub Check: ci-build-servers (dbr-echo-directus, ./echo/directus, Dockerfile, dbr-echo-directus)
🔇 Additional comments (1)
echo/frontend/src/components/participant/hooks/index.ts (1)

311-313: Signature reflow LGTM.

Style-only; no behavior change.

Comment on lines +59 to 60
retry: 20,
// When mutate is called:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Blindly bumping retries to 20 will DDoS your own API under load; make retries status-aware with exponential backoff + jitter.

Aligns with ECHO-428 (contention during concurrent recordings). Gate on non-transient 4xx and back off.

   return useMutation({
     mutationFn: uploadConversationChunk,
-    retry: 20,
+    retry: (failureCount, error) => {
+      const status = isAxiosError(error) ? error.response?.status : undefined;
+      // Don't retry on non-transient client errors
+      if (typeof status === "number" && [400, 401, 403, 404, 409, 410, 422].includes(status)) {
+        return false;
+      }
+      return failureCount < 8;
+    },
+    retryDelay: (attempt) =>
+      Math.min(30000, 1000 * 2 ** attempt) + Math.floor(Math.random() * 500),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
retry: 20,
// When mutate is called:
return useMutation({
mutationFn: uploadConversationChunk,
retry: (failureCount, error) => {
const status = isAxiosError(error) ? error.response?.status : undefined;
// Don't retry on non-transient client errors
if (
typeof status === "number" &&
[400, 401, 403, 404, 409, 410, 422].includes(status)
) {
return false;
}
// Retry up to 8 times for transient failures
return failureCount < 8;
},
retryDelay: (attempt) =>
Math.min(30000, 1000 * 2 ** attempt) + Math.floor(Math.random() * 500),
// When mutate is called:
});
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/hooks/index.ts around lines 59-60,
the retry count is hard-coded to 20 which can DDoS our API; change retry logic
to be status-aware and use exponential backoff with jitter: detect non-transient
4xx responses and do not retry them, retry transient errors (5xx, network
timeouts) with an increasing delay (baseDelay * 2^attempt) plus random jitter,
and cap the max attempts and max delay; implement this in the mutate/retry
handler so the backoff calculation and status checks determine whether to retry
rather than a fixed retry count.

spashii pushed a commit that referenced this pull request Nov 18, 2025
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Recording now stops automatically when a conversation is deleted or
access is revoked, preventing stuck or misleading states.
- Improved error handling to distinguish permanent errors (stop
recording) from transient ones (continue recording).
- Reduced unnecessary retry attempts for permanently missing/unavailable
conversations; transient errors still retry with limited attempts.
- Increased stability during ongoing recordings by continuing through
safe, transient fetch issues.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants