Report queued tasks the scheduling pass never considers

A task in retry backoff was filtered out before the candidate loop, so it
recorded no rejection at all: queued, apparently assignable, and silent. That is
the exact shape that made F5 take a live session to diagnose. It now reports
"retry backoff until <time>".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 23:42:26 +04:00
parent 0ead6d2d02
commit 4fbf3ac966
18 changed files with 1163 additions and 165 deletions
+13 -2
View File
@@ -17,9 +17,20 @@ describe('UI API client',()=>{
expect(fetch).toHaveBeenCalledWith('/v1/artifacts',expect.objectContaining({method:'POST',body:'report'}))
})
it('submits username and password to the browser login endpoint',async()=>{
const fetch=vi.fn().mockResolvedValue(new Response(null,{status:204}))
const fetch=vi.fn().mockResolvedValue(new Response(JSON.stringify({username:'operator'}),{status:200}))
vi.stubGlobal('fetch',fetch)
await api.login('operator','not stored in the browser')
await expect(api.login('operator','not stored in the browser')).resolves.toEqual({username:'operator'})
expect(fetch).toHaveBeenCalledWith('/v1/ui/session',expect.objectContaining({method:'POST',body:JSON.stringify({username:'operator',password:'not stored in the browser'})}))
})
it('treats a missing browser session as a normal signed-out state',async()=>{
const fetch=vi.fn().mockResolvedValue(new Response('unauthorized',{status:401}))
vi.stubGlobal('fetch',fetch)
await expect(api.session()).resolves.toBeUndefined()
})
it('updates account credentials through the session-gated account endpoint',async()=>{
const fetch=vi.fn().mockResolvedValue(new Response(JSON.stringify({username:'kami'}),{status:200}))
vi.stubGlobal('fetch',fetch)
await api.updateAccount({current_password:'old password',username:'kami',new_password:'new password'})
expect(fetch).toHaveBeenCalledWith('/v1/ui/account',expect.objectContaining({method:'PUT'}))
})
})
+16 -1
View File
@@ -1,4 +1,4 @@
import type { CreatedEvent, Detail, Overview } from './types'
import type { Account, CreatedEvent, Detail, Overview } from './types'
function sessionExpired(response: Response) {
if (response.status === 401) {
@@ -49,6 +49,13 @@ async function upload(body: string) {
return ((await response.json()) as { ref: string }).ref
}
async function session(): Promise<Account | undefined> {
const response = await fetch('/v1/ui/session', { credentials: 'same-origin' })
if (response.status === 401) return undefined
if (!response.ok) throw await responseError(response)
return response.json() as Promise<Account>
}
async function login(username: string, password: string) {
const response = await fetch('/v1/ui/session', {
method: 'POST',
@@ -57,6 +64,7 @@ async function login(username: string, password: string) {
body: JSON.stringify({ username, password }),
})
if (!response.ok) throw await responseError(response)
return response.json() as Promise<Account>
}
async function logout() {
@@ -68,8 +76,15 @@ async function logout() {
}
export const api = {
session,
login,
logout,
account: () => request<Account>('/v1/ui/account'),
updateAccount: (body: { current_password: string; username: string; new_password?: string }) =>
request<Account>('/v1/ui/account', {
method: 'PUT',
body: JSON.stringify(body),
}),
overview: () => request<Overview>('/v1/ui/overview'),
detail: (id: string) => request<Detail>(`/v1/ui/tasks/${id}`),
artifact: (ref: string) => text(`/v1/ui/artifacts/${ref}`),
+17 -1
View File
@@ -1,4 +1,11 @@
export type TaskState = 'queued' | 'leased' | 'blocked' | 'completed' | 'failed'
export type TaskState =
| 'queued'
| 'leased'
| 'needs_attention'
| 'blocked'
| 'in_review'
| 'completed'
| 'failed'
export type BlockReason =
| 'lease_failure'
@@ -8,8 +15,17 @@ export type BlockReason =
| 'handoff_validation'
| 'operator_block'
| 'system_error'
| 'trajectory_gate'
| 'human_decision'
| 'operator_required'
| 'unknown'
export interface Account {
username: string
created_at: string
updated_at: string
}
export interface SessionEvidence {
pane_id?: string
harness_id?: string