Follow a running production every few seconds

A started production kept showing "AI picks new topics" for a minute
or more: each schedule's status was read once a minute, and the refresh
right after starting got the read kept from before the start. That read
is now dropped when a production starts, and while the latest production
is under way its status is read every five seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-30 13:50:05 +07:00
parent 7a7f67d3e2
commit 677937eb98
2 changed files with 20 additions and 11 deletions

View File

@@ -397,9 +397,9 @@ export function planProgress(plan: ContentPlan) {
// from the last half minute, or one still on its way, answers both. forgetReads drops them after an action. // from the last half minute, or one still on its way, answers both. forgetReads drops them after an action.
const PRODUCTION_FRESH = 30_000 const PRODUCTION_FRESH = 30_000
const productionReads = new Map<number, { at: number; read: Promise<ProductionStatus> }>() const productionReads = new Map<number, { at: number; read: Promise<ProductionStatus> }>()
export function productionStatus(id: number): Promise<ProductionStatus> { export function productionStatus(id: number, maxAge = PRODUCTION_FRESH): Promise<ProductionStatus> {
const kept = productionReads.get(id) const kept = productionReads.get(id)
if (kept && Date.now() - kept.at < PRODUCTION_FRESH) return kept.read if (kept && Date.now() - kept.at < maxAge) return kept.read
const read = request<ProductionStatus>(`${AGENTS}/${id}/content`) const read = request<ProductionStatus>(`${AGENTS}/${id}/content`)
productionReads.set(id, { at: Date.now(), read }) productionReads.set(id, { at: Date.now(), read })
read.catch(() => { if (productionReads.get(id)?.read === read) productionReads.delete(id) }) read.catch(() => { if (productionReads.get(id)?.read === read) productionReads.delete(id) })

View File

@@ -7,8 +7,8 @@ import { toast } from '../components/Toast'
import { describeRule } from '../express/parse' import { describeRule } from '../express/parse'
import { locale, t, type Key } from '../i18n' import { locale, t, type Key } from '../i18n'
import { import {
archiveSchedule, confirmProduction, isMine, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, scheduleOwners, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule, archiveSchedule, confirmProduction, forgetProductionReads, isMine, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, scheduleOwners, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type Owners, type RunSummary, type Schedule, planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type Owners, type ProductionStatus, type RunSummary, type Schedule,
} from '../express/schedules' } from '../express/schedules'
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
@@ -152,13 +152,20 @@ function ScheduleRow({ schedule, owners }: { schedule: Schedule; owners?: Owners
} }
// Content schedules report this week's progress from the engine's production period. Each row // Content schedules report this week's progress from the engine's production period. Each row
// asks for its own, so it is read once a minute; starting a production refreshes it at once. // asks for its own, so it is read once a minute; while its latest production is under way it is
const useProduction = (agentId: number) => useQuery({ // read every few seconds, so each step shows as it happens. Starting a production refreshes it at once.
queryKey: ['production', agentId], const LIVE_POLL = 5000
queryFn: () => productionStatus(agentId), const isLive = (s: ProductionStatus | undefined) => !!s?.plans[0] && PLAN_ACTIVE.has(s.plans[0].status)
refetchInterval: 60_000, function useProduction(agentId: number) {
staleTime: 60_000, const queryClient = useQueryClient()
}) const key = ['production', agentId]
return useQuery({
queryKey: key,
queryFn: () => productionStatus(agentId, isLive(queryClient.getQueryData<ProductionStatus>(key)) ? LIVE_POLL : undefined),
refetchInterval: q => (isLive(q.state.data) ? LIVE_POLL : 60_000),
staleTime: q => (isLive(q.state.data) ? LIVE_POLL : 60_000),
})
}
function ProductionLine({ agentId, active }: { agentId: number; active: boolean }) { function ProductionLine({ agentId, active }: { agentId: number; active: boolean }) {
const status = useProduction(agentId) const status = useProduction(agentId)
@@ -218,6 +225,8 @@ function ProducePanel({ schedule: { agent, rule }, onClose }: { schedule: Schedu
const start = useMutation({ const start = useMutation({
mutationFn: (plan: ContentPlan) => confirmProduction(agent.id, plan), mutationFn: (plan: ContentPlan) => confirmProduction(agent.id, plan),
onSuccess: () => { onSuccess: () => {
// The read kept from before the start still shows no production; it must not answer the refresh.
forgetProductionReads()
queryClient.invalidateQueries({ queryKey: ['production', agent.id] }) queryClient.invalidateQueries({ queryKey: ['production', agent.id] })
queryClient.invalidateQueries({ queryKey: ['results'] }) queryClient.invalidateQueries({ queryKey: ['results'] })
onClose() onClose()