From acbccd10c0ea609c1fefdfa224ba6f92c82d94ce Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Fri, 4 Sep 2026 07:01:54 +0000 Subject: [PATCH] fix(cos): warn when pending on-demand tasks can't run because the daemon is stopped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit triggerOnDemandTask() writes a request to schedule.onDemandRequests regardless of daemon state, but dequeueNextTask() bails immediately when isDaemonRunning() is false — so a stopped daemon leaves the "Pending On-Demand Tasks" banner showing a request that will never be picked up, with no indication why. Pass the already-live status.running down from ChiefOfStaff so the banner switches to a warning and the queue toast tells the user the daemon needs to be started, instead of looking silently stuck. --- .../src/components/cos/tabs/ScheduleTab.jsx | 26 ++++++--- .../components/cos/tabs/ScheduleTab.test.jsx | 54 +++++++++++++++++++ client/src/pages/ChiefOfStaff.jsx | 2 +- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/client/src/components/cos/tabs/ScheduleTab.jsx b/client/src/components/cos/tabs/ScheduleTab.jsx index 04a56ad54b..bc2673fdcf 100644 --- a/client/src/components/cos/tabs/ScheduleTab.jsx +++ b/client/src/components/cos/tabs/ScheduleTab.jsx @@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router'; import { AlertCircle, RefreshCw } from 'lucide-react'; import toast from '../../ui/Toast'; import * as api from '../../../services/api'; -import { formatDateTime, formatTimeOfDaySeconds } from '../../../utils/formatters'; +import { formatDateTime, formatTimeOfDaySeconds, timeAgo } from '../../../utils/formatters'; import Banner from '../../ui/Banner'; import { CodeReviewDefaultsProvider } from '../../../hooks/useCodeReviewDefaults'; import { useAppOverrideActions } from '../../../hooks/useAppOverrideActions'; @@ -40,7 +40,7 @@ function mergeOnDemandRequest(schedule, request) { // passed down — same convention as TasksTab/AgentsTab — so this tab's provider/ // model pickers stay live without standing up a second independent poll of the // same data. -export default function ScheduleTab({ apps, providers, providersLoaded, activeProviderId }) { +export default function ScheduleTab({ apps, providers, providersLoaded, activeProviderId, daemonRunning }) { const [searchParams, setSearchParams] = useSearchParams(); const [schedule, setSchedule] = useState(null); const [loading, setLoading] = useState(true); @@ -101,7 +101,12 @@ export default function ScheduleTab({ apps, providers, providersLoaded, activePr if (!result?.success) return null; const appName = appId ? apps?.find(app => app.id === appId)?.name || 'selected app' : null; - toast.success(`Queued ${taskType} request${appName ? ` for ${appName}` : ''} — it will appear in Tasks when evaluation begins`); + const queuedMsg = `Queued ${taskType} request${appName ? ` for ${appName}` : ''}`; + if (daemonRunning === false) { + toast.error(`${queuedMsg} — but the CoS daemon is stopped, so it will not run until you start it`); + } else { + toast.success(`${queuedMsg} — it will appear in Tasks when evaluation begins`); + } // The POST returns the persisted request. Paint it immediately instead of // waiting for a second round trip; the evaluator may drain it into Tasks @@ -112,7 +117,7 @@ export default function ScheduleTab({ apps, providers, providersLoaded, activePr } fetchSchedule(); return result.request || true; - }, [apps, fetchSchedule]); + }, [apps, fetchSchedule, daemonRunning]); const handleTriggerAppImprovement = handleTriggerTask; @@ -170,11 +175,20 @@ export default function ScheduleTab({ apps, providers, providersLoaded, activePr )} {schedule.onDemandRequests?.length > 0 && ( - + + {daemonRunning === false && ( +
+ The CoS daemon is stopped, so these requests will not run until it's started — use the Start button above. +
+ )}
{schedule.onDemandRequests.map(req => (
- {req.taskType}{req.appId ? ` (${apps?.find(app => app.id === req.appId)?.name || req.appId})` : ''} - requested {formatTimeOfDaySeconds(req.requestedAt)} + {req.taskType}{req.appId ? ` (${apps?.find(app => app.id === req.appId)?.name || req.appId})` : ''} - requested {formatTimeOfDaySeconds(req.requestedAt)} ({timeAgo(req.requestedAt)})
))}
diff --git a/client/src/components/cos/tabs/ScheduleTab.test.jsx b/client/src/components/cos/tabs/ScheduleTab.test.jsx index b5a40fbdf2..987935ea60 100644 --- a/client/src/components/cos/tabs/ScheduleTab.test.jsx +++ b/client/src/components/cos/tabs/ScheduleTab.test.jsx @@ -98,4 +98,58 @@ describe('ScheduleTab on-demand feedback', () => { expect(screen.getByText('Pending On-Demand Tasks')).toBeVisible(); expect(screen.getByText(/review \(Example App\) - requested/)).toBeVisible(); }); + + it('warns instead of celebrating a queued request when the CoS daemon is stopped', async () => { + const user = userEvent.setup(); + const request = { + id: 'request-2', + taskType: 'review', + appId: 'app-1', + requestedAt: '2026-09-01T12:00:00.000Z', + }; + api.getCodeReviewDefaults.mockResolvedValue({}); + api.getCosSchedule + .mockResolvedValueOnce({ + improvementEnabled: true, + tasks: { + review: { + type: 'on-demand', + enabled: true, + enabledAppCount: 1, + totalAppCount: 1, + invocation: { userInvokable: true }, + }, + }, + onDemandRequests: [], + }) + // Hold the background refresh so this assertion proves the + // optimistically-painted request (and its daemon-stopped warning) + // stays visible without waiting on a second round trip. + .mockReturnValueOnce(new Promise(() => {})); + api.triggerCosOnDemandTask.mockResolvedValue({ success: true, request }); + + render( + + + , + ); + + await user.click(await screen.findByRole('button', { name: /Run on App/i })); + await user.click(screen.getByRole('button', { name: 'Example App' })); + + await waitFor(() => expect(api.triggerCosOnDemandTask).toHaveBeenCalledWith( + 'review', + 'app-1', + { silent: true }, + )); + expect(toast.error).toHaveBeenCalledWith( + 'Queued review request for Example App — but the CoS daemon is stopped, so it will not run until you start it', + ); + expect(await screen.findByText(/CoS daemon is stopped/)).toBeVisible(); + }); }); diff --git a/client/src/pages/ChiefOfStaff.jsx b/client/src/pages/ChiefOfStaff.jsx index 32f69a4426..e9b4fce27a 100644 --- a/client/src/pages/ChiefOfStaff.jsx +++ b/client/src/pages/ChiefOfStaff.jsx @@ -1237,7 +1237,7 @@ export default function ChiefOfStaff() { {activeTab === 'schedule' && (
}> - +
)}