Keep a schedule's last results while the engine is busy
A schedule the proxy's rate limit answered with 503 was left out of that poll, so every count on Production dipped for a minute and came back on the next. A busy schedule is now asked once more after a pause, and if it still fails its results from the last poll are shown. A refused one (4xx) is still left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// content plan item that ordered them), rendered videos (video-drafts) and web sources found by
|
||||
// research (source-drafts). Audio and SCORM are Express kinds built from Compose outputs: an audio
|
||||
// lesson is a narration summary, a SCORM module is the summary + quiz pair one plan item produced.
|
||||
import { request } from '../api/client'
|
||||
import { EngineError, request, sleep } from '../api/client'
|
||||
import { t } from '../i18n'
|
||||
import { kindLabel, type ContentKind, type Rule } from './parse'
|
||||
import { expressAgents, MAX_ATTEMPTS, PLAN_ACTIVE, productionStatus, retryItem, runNow, type PlanItem, type Schedule } from './schedules'
|
||||
@@ -93,7 +93,9 @@ async function materialsOf(schedule: Schedule): Promise<Material[]> {
|
||||
const kind = rule.type === 'content' ? rule.kind : 'summary'
|
||||
const [drafts, status] = await Promise.all([
|
||||
request<RawMaterial[]>(`${AGENTS}/${agent.id}/material-drafts`),
|
||||
productionStatus(agent.id).catch(() => null),
|
||||
// Without its plan a schedule still has drafts; a busy engine fails the whole read instead, so
|
||||
// the last complete one is shown rather than drafts missing their titles and planned pieces.
|
||||
productionStatus(agent.id).catch(e => { if (passing(e)) throw e; return null }),
|
||||
])
|
||||
const items = new Map<number, PlanItem & { planId: number }>()
|
||||
for (const plan of status?.plans ?? []) {
|
||||
@@ -201,6 +203,35 @@ export async function settleLimited<T, R>(items: T[], limit: number, fn: (item:
|
||||
|
||||
const PER_SCHEDULE_LIMIT = 4
|
||||
|
||||
// The proxy's rate limit (503/429), a 5xx or no answer at all passes; a 4xx (e.g. a deleted agent) does not.
|
||||
const passing = (e: unknown) => e instanceof EngineError && (e.status === 0 || e.status === 429 || e.status >= 500)
|
||||
|
||||
/** Wraps a per-schedule read so a passing failure does not change what the page shows: a busy
|
||||
* engine is asked once more after a pause, and if it still fails, the schedule's results from the
|
||||
* last poll that read them are used. Dropping them instead made every count on the page dip for
|
||||
* one poll and come back on the next. */
|
||||
export function remembering<T>(fn: (s: Schedule) => Promise<T[]>, pause = 3000) {
|
||||
const last = new Map<number, T[]>()
|
||||
return async (s: Schedule): Promise<T[]> => {
|
||||
try {
|
||||
const read = await fn(s).catch(async e => {
|
||||
if (!(e instanceof EngineError && (e.status === 503 || e.status === 429))) throw e
|
||||
await sleep(pause)
|
||||
return fn(s)
|
||||
})
|
||||
last.set(s.agent.id, read)
|
||||
return read
|
||||
} catch (e) {
|
||||
const kept = last.get(s.agent.id)
|
||||
if (kept && passing(e)) return kept
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const readMaterials = remembering(materialsOf)
|
||||
const readSources = remembering(sourcesOf)
|
||||
|
||||
// Results cost one request per schedule (every video schedule's drafts), so they are read once a
|
||||
// minute, and a page opened or refocused within that minute reuses what was read. An action on a
|
||||
// result or schedule still refreshes them at once.
|
||||
@@ -211,8 +242,8 @@ export async function listResults(): Promise<{ materials: Material[]; sources: F
|
||||
const content = schedules.filter(s => s.rule.type === 'content')
|
||||
const research = schedules.filter(s => s.rule.type === 'research')
|
||||
// One shared budget: research runs after content rather than alongside it.
|
||||
const materials = await settleLimited(content, PER_SCHEDULE_LIMIT, materialsOf)
|
||||
const sources = await settleLimited(research, PER_SCHEDULE_LIMIT, sourcesOf)
|
||||
const materials = await settleLimited(content, PER_SCHEDULE_LIMIT, readMaterials)
|
||||
const sources = await settleLimited(research, PER_SCHEDULE_LIMIT, readSources)
|
||||
const newest = <T extends { createdAt: string }>(a: T, b: T) => b.createdAt.localeCompare(a.createdAt)
|
||||
return { materials: keepSettled(materials).sort(newest), sources: keepSettled(sources).sort(newest) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user