fix(mothership): stop persisting log resources from get_workflow_logs and self-heal stale log panel entries (#4424)

* fix(mothership): stop persisting log resources from get_workflow_logs and self-heal stale log panel entries

* fix(mothership): skip retries on 404 in useLogDetail for instant self-heal

* fix(mothership): simplify onNotFoundRef sync to inline assignment
This commit is contained in:
Waleed
2026-05-04 12:44:16 -07:00
committed by GitHub
parent 3af6c25b97
commit 578fc505ec
5 changed files with 39 additions and 19 deletions
@@ -1,6 +1,6 @@
'use client'
import { lazy, memo, Suspense, useEffect, useMemo } from 'react'
import { lazy, memo, Suspense, useEffect, useMemo, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { Square } from 'lucide-react'
import { useRouter } from 'next/navigation'
@@ -13,6 +13,7 @@ import {
SquareArrowUpRight,
WorkflowX,
} from '@/components/emcn/icons'
import { isApiClientError } from '@/lib/api/client/errors'
import type { FilePreviewSession } from '@/lib/copilot/request/session'
import {
cancelRunToolExecution,
@@ -70,6 +71,7 @@ interface ResourceContentProps {
previewSession?: FilePreviewSession | null
genericResourceData?: GenericResourceData
previewContextKey?: string
onNotFound?: (resourceId: string) => void
}
/**
@@ -86,6 +88,7 @@ export const ResourceContent = memo(function ResourceContent({
previewSession,
genericResourceData,
previewContextKey,
onNotFound,
}: ResourceContentProps) {
const streamFileName = previewSession?.fileName || 'file.md'
const syntheticFile = useMemo(() => {
@@ -179,7 +182,13 @@ export const ResourceContent = memo(function ResourceContent({
return <EmbeddedFolder key={resource.id} workspaceId={workspaceId} folderId={resource.id} />
case 'log':
return <EmbeddedLog key={resource.id} logId={resource.id} />
return (
<EmbeddedLog
key={resource.id}
logId={resource.id}
onNotFound={onNotFound ? () => onNotFound(resource.id) : undefined}
/>
)
case 'generic':
return (
@@ -618,10 +627,20 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
interface EmbeddedLogProps {
logId: string
onNotFound?: () => void
}
function EmbeddedLog({ logId }: EmbeddedLogProps) {
const { data: log, isLoading } = useLogDetail(logId)
function EmbeddedLog({ logId, onNotFound }: EmbeddedLogProps) {
const { data: log, isLoading, error } = useLogDetail(logId)
const onNotFoundRef = useRef(onNotFound)
onNotFoundRef.current = onNotFound
useEffect(() => {
if (isApiClientError(error) && error.status === 404) {
onNotFoundRef.current?.()
}
}, [error])
if (isLoading) return LOADING_SKELETON
@@ -128,6 +128,7 @@ export const MothershipView = memo(
previewSession={previewForActive}
genericResourceData={active.type === 'generic' ? genericResourceData : undefined}
previewContextKey={chatId}
onNotFound={(resourceId) => onRemoveResource('log', resourceId)}
/>
) : (
<div className='flex h-full items-center justify-center text-[var(--text-muted)] text-sm'>
@@ -1420,6 +1420,18 @@ export function useChat(
const removeResource = useCallback((resourceType: MothershipResourceType, resourceId: string) => {
setResources((prev) => prev.filter((r) => !(r.type === resourceType && r.id === resourceId)))
setActiveResourceId((prev) => (prev === resourceId ? null : prev))
const persistChatId = chatIdRef.current ?? selectedChatIdRef.current
if (persistChatId) {
// boundary-raw-fetch: fire-and-forget side-effect; intentionally avoids requestJson's response parsing/throw semantics so a transient failure cannot interrupt the caller
fetch('/api/mothership/chat/resources', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chatId: persistChatId, resourceType, resourceId }),
}).catch((err) => {
logger.warn('Failed to persist resource removal', err)
})
}
}, [])
const reorderResources = useCallback((newOrder: MothershipResource[]) => {
+3
View File
@@ -7,6 +7,7 @@ import {
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { isApiClientError } from '@/lib/api/client/errors'
import { requestJson } from '@/lib/api/client/request'
import {
cancelWorkflowExecutionContract,
@@ -194,6 +195,8 @@ export function useLogDetail(logId: string | undefined, options?: UseLogDetailOp
enabled: Boolean(logId) && (options?.enabled ?? true),
refetchInterval: options?.refetchInterval ?? false,
staleTime: 30 * 1000,
retry: (failureCount, err) =>
!(isApiClientError(err) && err.status === 404) && failureCount < 3,
})
}
@@ -7,7 +7,6 @@ import {
FunctionExecute,
GenerateImage,
GenerateVisualization,
GetWorkflowLogs,
Knowledge,
KnowledgeBase,
UserTable,
@@ -30,7 +29,6 @@ const RESOURCE_TOOL_NAMES: Set<string> = new Set([
Knowledge.id,
GenerateVisualization.id,
GenerateImage.id,
GetWorkflowLogs.id,
])
export function isResourceToolName(toolName: string): boolean {
@@ -214,19 +212,6 @@ export function extractResourcesFromToolResult(
return resources
}
case GetWorkflowLogs.id: {
const entries = Array.isArray(output) ? output : Array.isArray(result.data) ? result.data : []
const resources: ChatResource[] = []
for (const entry of entries) {
const rec = asRecord(entry)
const logId = rec.id as string | undefined
if (logId) {
resources.push({ type: 'log', id: logId, title: 'Log' })
}
}
return resources
}
default:
return []
}