fix: address remaining review feedback

Backend:
- Make invalidate_session_cache best-effort with try-except

Frontend:
- Fix polling bug: use recursive scheduling so polling continues after each refetch
- Add toolName fallback in helpers.ts parseToolResponse for operation_* types
- Make PendingOperationWidget title dynamic based on toolName instead of hardcoded "Creating Agent"
This commit is contained in:
Zamil Majdy
2026-01-27 11:32:33 -06:00
parent 4371c48814
commit 9aebc1de67
4 changed files with 63 additions and 24 deletions

View File

@@ -299,10 +299,15 @@ async def invalidate_session_cache(session_id: str) -> None:
"""Invalidate a chat session from Redis cache.
Used by background tasks to ensure fresh data is loaded on next access.
This is best-effort - Redis failures are logged but don't fail the operation.
"""
redis_key = _get_session_cache_key(session_id)
async_redis = await get_redis_async()
await async_redis.delete(redis_key)
try:
redis_key = _get_session_cache_key(session_id)
async_redis = await get_redis_async()
await async_redis.delete(redis_key)
except Exception as e:
# Best-effort: log but don't fail - cache will expire naturally
logger.warning(f"Failed to invalidate session cache for {session_id}: {e}")
async def _get_session_from_db(session_id: str) -> ChatSession | None:

View File

@@ -344,7 +344,7 @@ export function parseToolResponse(
if (responseType === "operation_started") {
return {
type: "operation_started",
toolName: parsedResult.tool_name as string,
toolName: (parsedResult.tool_name as string) || toolName,
operationId: (parsedResult.operation_id as string) || "",
message:
(parsedResult.message as string) ||
@@ -355,7 +355,7 @@ export function parseToolResponse(
if (responseType === "operation_pending") {
return {
type: "operation_pending",
toolName: parsedResult.tool_name as string,
toolName: (parsedResult.tool_name as string) || toolName,
operationId: (parsedResult.operation_id as string) || "",
message:
(parsedResult.message as string) ||
@@ -366,7 +366,7 @@ export function parseToolResponse(
if (responseType === "operation_in_progress") {
return {
type: "operation_in_progress",
toolName: parsedResult.tool_name as string,
toolName: (parsedResult.tool_name as string) || toolName,
toolCallId: (parsedResult.tool_call_id as string) || "",
message:
(parsedResult.message as string) ||

View File

@@ -19,6 +19,19 @@ interface Props {
className?: string;
}
function getOperationTitle(toolName?: string): string {
if (!toolName) return "Operation";
// Convert tool name to human-readable format
// e.g., "create_agent" -> "Creating Agent", "edit_agent" -> "Editing Agent"
if (toolName === "create_agent") return "Creating Agent";
if (toolName === "edit_agent") return "Editing Agent";
// Default: capitalize and format tool name
return toolName
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
export function PendingOperationWidget({
status,
message,
@@ -30,6 +43,8 @@ export function PendingOperationWidget({
const isCompleted = status === "completed";
const isError = status === "error";
const operationTitle = getOperationTitle(toolName);
return (
<div
className={cn(
@@ -66,9 +81,9 @@ export function PendingOperationWidget({
<Card className="space-y-2 p-4">
<div>
<Text variant="h4" className="mb-1 text-slate-900">
{isPending && "Creating Agent"}
{isCompleted && "Operation Complete"}
{isError && "Operation Failed"}
{isPending && operationTitle}
{isCompleted && `${operationTitle} Complete`}
{isError && `${operationTitle} Failed`}
</Text>
<Text variant="small" className="text-slate-600">
{message}

View File

@@ -132,6 +132,9 @@ export function useChatSession({
// Poll for updates when there are pending operations (long poll - 10s intervals with backoff)
const pollAttemptRef = useRef(0);
const hasPendingOperationsRef = useRef(hasPendingOperations);
hasPendingOperationsRef.current = hasPendingOperations;
useEffect(
function pollForPendingOperations() {
if (!sessionId || !hasPendingOperations) {
@@ -139,27 +142,43 @@ export function useChatSession({
return;
}
let cancelled = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
// Calculate delay with exponential backoff: 10s, 15s, 20s, 25s, 30s (max)
const baseDelay = 10000;
const maxDelay = 30000;
const delay = Math.min(
baseDelay + pollAttemptRef.current * 5000,
maxDelay,
);
const timeoutId = setTimeout(async () => {
console.info(
`[useChatSession] Polling for pending operation updates (attempt ${pollAttemptRef.current + 1})`,
function schedule() {
const delay = Math.min(
baseDelay + pollAttemptRef.current * 5000,
maxDelay,
);
pollAttemptRef.current += 1;
try {
await refetch();
} catch (err) {
console.error("[useChatSession] Poll failed:", err);
}
}, delay);
timeoutId = setTimeout(async () => {
if (cancelled) return;
console.info(
`[useChatSession] Polling for pending operation updates (attempt ${pollAttemptRef.current + 1})`,
);
pollAttemptRef.current += 1;
try {
await refetch();
} catch (err) {
console.error("[useChatSession] Poll failed:", err);
} finally {
// Continue polling if still pending and not cancelled
if (!cancelled && hasPendingOperationsRef.current) {
schedule();
}
}
}, delay);
}
return () => clearTimeout(timeoutId);
schedule();
return () => {
cancelled = true;
if (timeoutId) clearTimeout(timeoutId);
};
},
[sessionId, hasPendingOperations, refetch],
);