agent-relay-server 0.98.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +70 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-C4AeEHdz.js → MessageBubble-DiorZXyB.js} +2 -2
- package/public/assets/{MessageBubble-C4AeEHdz.js.map → MessageBubble-DiorZXyB.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-cwPUGFXA.js → PlanGraphPanel-DY1p8v4p.js} +2 -2
- package/public/assets/{PlanGraphPanel-cwPUGFXA.js.map → PlanGraphPanel-DY1p8v4p.js.map} +1 -1
- package/public/assets/{activity-BWGwnSg5.js → activity-rIxqUtFC.js} +2 -2
- package/public/assets/{activity-BWGwnSg5.js.map → activity-rIxqUtFC.js.map} +1 -1
- package/public/assets/{agent-profiles-C6N9cTJg.js → agent-profiles-Cu-VkJm4.js} +2 -2
- package/public/assets/{agent-profiles-C6N9cTJg.js.map → agent-profiles-Cu-VkJm4.js.map} +1 -1
- package/public/assets/{agents-FUBxhCEU.js → agents-dRHOBDUe.js} +2 -2
- package/public/assets/{agents-FUBxhCEU.js.map → agents-dRHOBDUe.js.map} +1 -1
- package/public/assets/{automation-CMketzfB.js → automation-B0cyCT5l.js} +2 -2
- package/public/assets/{automation-CMketzfB.js.map → automation-B0cyCT5l.js.map} +1 -1
- package/public/assets/{chat-BV_pnRhk.js → chat-jit-e6S_.js} +2 -2
- package/public/assets/{chat-BV_pnRhk.js.map → chat-jit-e6S_.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DzHQTBmn.js → formatted-body-impl-ZwgTH3jh.js} +2 -2
- package/public/assets/{formatted-body-impl-DzHQTBmn.js.map → formatted-body-impl-ZwgTH3jh.js.map} +1 -1
- package/public/assets/index-B95vV-dd.css +2 -0
- package/public/assets/index-GoC6YrKn.js +23 -0
- package/public/assets/index-GoC6YrKn.js.map +1 -0
- package/public/assets/{insights-C_pJNcFD.js → insights-C3mlXZdd.js} +2 -2
- package/public/assets/{insights-C_pJNcFD.js.map → insights-C3mlXZdd.js.map} +1 -1
- package/public/assets/{maintenance-Ckz5BAor.js → maintenance-DL1lnqmG.js} +2 -2
- package/public/assets/{maintenance-Ckz5BAor.js.map → maintenance-DL1lnqmG.js.map} +1 -1
- package/public/assets/{managed-agents-BbZclnqP.js → managed-agents-7A6WH4Rn.js} +2 -2
- package/public/assets/{managed-agents-BbZclnqP.js.map → managed-agents-7A6WH4Rn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CtuHw6Mm.js → markdown-preview-impl-THat8xv0.js} +2 -2
- package/public/assets/{markdown-preview-impl-CtuHw6Mm.js.map → markdown-preview-impl-THat8xv0.js.map} +1 -1
- package/public/assets/{memory-C5DK3rhW.js → memory-C349UJe9.js} +2 -2
- package/public/assets/{memory-C5DK3rhW.js.map → memory-C349UJe9.js.map} +1 -1
- package/public/assets/{messages-CsjtiFEb.js → messages-DXoLFj2P.js} +2 -2
- package/public/assets/{messages-CsjtiFEb.js.map → messages-DXoLFj2P.js.map} +1 -1
- package/public/assets/{orchestrators-D9fMiBRy.js → orchestrators-CTXu4jAn.js} +2 -2
- package/public/assets/{orchestrators-D9fMiBRy.js.map → orchestrators-CTXu4jAn.js.map} +1 -1
- package/public/assets/{overview-BLWoCnNQ.js → overview-dWiN6TyN.js} +2 -2
- package/public/assets/{overview-BLWoCnNQ.js.map → overview-dWiN6TyN.js.map} +1 -1
- package/public/assets/{pairs-HnvpLz4C.js → pairs-KIXektDv.js} +2 -2
- package/public/assets/{pairs-HnvpLz4C.js.map → pairs-KIXektDv.js.map} +1 -1
- package/public/assets/{registry-BqAS3ADV.js → registry-C-Y41yGB.js} +2 -2
- package/public/assets/{registry-BqAS3ADV.js.map → registry-C-Y41yGB.js.map} +1 -1
- package/public/assets/{security-CWgtcQ2X.js → security-BUymkwuj.js} +2 -2
- package/public/assets/{security-CWgtcQ2X.js.map → security-BUymkwuj.js.map} +1 -1
- package/public/assets/{select-UdIYiKFs.js → select-DV25WRP5.js} +2 -2
- package/public/assets/{select-UdIYiKFs.js.map → select-DV25WRP5.js.map} +1 -1
- package/public/assets/{settings-ruDxPuza.js → settings-C1En58bx.js} +2 -2
- package/public/assets/{settings-ruDxPuza.js.map → settings-C1En58bx.js.map} +1 -1
- package/public/assets/{tasks-DomQFwZP.js → tasks-BkpnKA63.js} +2 -2
- package/public/assets/{tasks-DomQFwZP.js.map → tasks-BkpnKA63.js.map} +1 -1
- package/public/assets/{teams-CXNp0U3B.js → teams-D-UYp7yH.js} +2 -2
- package/public/assets/{teams-CXNp0U3B.js.map → teams-D-UYp7yH.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DiWBtzvZ.js → terminal-viewer-impl-Bif-tkvD.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DiWBtzvZ.js.map → terminal-viewer-impl-Bif-tkvD.js.map} +1 -1
- package/public/assets/{user-avatar-DBs9Koqp.js → user-avatar--czLv_AJ.js} +2 -2
- package/public/assets/{user-avatar-DBs9Koqp.js.map → user-avatar--czLv_AJ.js.map} +1 -1
- package/public/assets/{work-queue-BD29QuLd.js → work-queue-DN4oTe7w.js} +2 -2
- package/public/assets/{work-queue-BD29QuLd.js.map → work-queue-DN4oTe7w.js.map} +1 -1
- package/public/index.html +2 -2
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/db/provider-quotas.ts +22 -6
- package/src/notification-types.ts +3 -1
- package/src/quota-advisory.ts +2 -0
- package/src/quota-band-transition.ts +147 -0
- package/src/quota.ts +1 -1
- package/src/routes/index.ts +2 -1
- package/src/routes/provider-quotas.ts +18 -0
- package/src/routing-policy-push.ts +1 -1
- package/src/security.ts +2 -0
- package/src/spawn-targets.ts +41 -1
- package/public/assets/index-CYLrIuJT.css +0 -2
- package/public/assets/index-CdEAo41I.js +0 -23
- package/public/assets/index-CdEAo41I.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{Qn as t,Xn as n,dr as r,er as i}from"./lucide-react-BYZuAhQs.js";import{_ as a,rt as o}from"./display-DRiGi1zj.js";import{D as s}from"./index-
|
|
2
|
-
//# sourceMappingURL=user-avatar
|
|
1
|
+
import{r as e}from"./chunk-CilyBKbf.js";import{Qn as t,Xn as n,dr as r,er as i}from"./lucide-react-BYZuAhQs.js";import{_ as a,rt as o}from"./display-DRiGi1zj.js";import{D as s}from"./index-GoC6YrKn.js";var c=e(r(),1),l=t(),u={id:o};function d({user:e,address:t,distinct:r=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let n=await i(d);if(e)return;t=URL.createObjectURL(n),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:n(`w-7 h-7 rounded-md object-cover`,o)});if(r){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:n(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(t),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
|
|
2
|
+
//# sourceMappingURL=user-avatar--czLv_AJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-avatar
|
|
1
|
+
{"version":3,"file":"user-avatar--czLv_AJ.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Dn as e,Qn as t,hn as n,i as r,pt as i}from"./lucide-react-BYZuAhQs.js";import{l as a,t as o}from"./store-pKbf_WYd.js";import{K as s,ct as c,d as l}from"./display-DRiGi1zj.js";import{t as u}from"./badge-BLar6FjP.js";import{t as d}from"./button-BrIB69qd.js";import{W as f,X as p,u as m}from"./index-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
1
|
+
import{Dn as e,Qn as t,hn as n,i as r,pt as i}from"./lucide-react-BYZuAhQs.js";import{l as a,t as o}from"./store-pKbf_WYd.js";import{K as s,ct as c,d as l}from"./display-DRiGi1zj.js";import{t as u}from"./badge-BLar6FjP.js";import{t as d}from"./button-BrIB69qd.js";import{W as f,X as p,u as m}from"./index-GoC6YrKn.js";import{r as h,t as g}from"./card-lhnFnBoz.js";var _=t(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?l(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:t,now:i,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),s(i,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
|
|
2
|
+
//# sourceMappingURL=work-queue-DN4oTe7w.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-queue-BD29QuLd.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"work-queue-DN4oTe7w.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"sXAWM,EAAwC,CAC5C,KAAM,kDACN,QAAS,wDACT,QAAS,2DACT,YAAa,wDACd,CAED,SAAgB,GAAgB,CAC9B,IAAM,EAAM,GAAQ,CACd,EAAa,EAAe,GAAM,EAAE,WAAW,CAC/C,EAAU,EAAe,GAAM,EAAE,QAAQ,CACzC,EAAM,EAAe,GAAM,EAAE,IAAI,CACjC,EAAc,EAAe,GAAM,EAAE,YAAY,CACjD,EAAU,EAAe,GAAM,EAAE,QAAQ,CACzC,EAAiB,EAAe,GAAM,EAAE,eAAe,CACvD,EAAY,EAAe,GAAM,EAAE,UAAU,CAC7C,EAAc,EAAe,GAAM,EAAE,YAAY,CACjD,EAAkB,EAAe,GAAM,EAAE,gBAAgB,CACzD,EAAqB,EAAe,GAAM,EAAE,mBAAmB,CAE/D,EAAgB,GAAkB,CAClC,EAAQ,GAAmB,CAE3B,EAAiB,EAAM,OAAQ,GAAM,EAAE,UAAU,CAAC,OAExD,SAAS,EAAY,EAAqB,CACxC,GAAI,CAAC,EAAQ,KAAM,CAAE,EAAU,aAAc,mCAAmC,CAAE,OAC9E,EAAK,aAAe,QAAU,EAAK,KAAM,EAAY,EAAK,KAAK,GAAG,CAC7D,EAAK,aAAe,WAAa,EAAK,SAAS,EAAQ,EAAK,QAAQ,GAAG,CAGlF,SAAS,EAAa,EAAqB,CAEzC,EAAY,cAAe,WADb,EAAK,QAAU,EAAK,aAAe,OAAS,SAAS,EAAK,KAAO,YAAY,EAAK,MACpD,+BAAkC,CACxE,EAAK,aAAe,QAAU,EAAK,KAAM,EAAmB,EAAK,KAAM,WAAW,CAC7E,EAAK,aAAe,WAAa,EAAK,SAAS,EAAgB,EAAK,QAAQ,GAAG,EACxF,CAGJ,SAAS,EAAa,EAAqB,CACrC,EAAK,MAAM,EAAe,EAAK,KAAK,CAG1C,OACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,qBAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,6DAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,mCAAf,EACE,EAAA,EAAA,KAAC,EAAD,CAAY,UAAU,gCAAkC,CAAA,EACxD,EAAA,EAAA,KAAC,KAAD,CAAI,UAAU,iCAAwB,aAAe,CAAA,CACpD,EAAiB,IAChB,EAAA,EAAA,MAAC,EAAD,CAAO,UAAU,wEAAjB,CACG,EAAe,aACV,GAEN,IACN,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,mCAAf,EACE,EAAA,EAAA,KAAC,QAAD,CAAO,UAAU,kDAAyC,WAAgB,CAAA,EAC1E,EAAA,EAAA,MAAC,SAAD,CACE,UAAU,mEACV,MAAO,EAAQ,KACf,SAAW,GAAM,EAAI,CAAE,QAAS,CAAE,GAAG,EAAS,KAAM,EAAE,OAAO,MAAO,CAAE,CAAC,UAHzE,EAKE,EAAA,EAAA,KAAC,SAAD,CAAQ,MAAM,YAAG,kBAAwB,CAAA,CACxC,EAAc,IAAK,IAAM,EAAA,EAAA,KAAC,SAAD,CAAmB,MAAO,EAAE,YAAK,EAAY,EAAE,CAAU,CAA5C,EAAE,GAA0C,CAAC,CAC7E,GACL,GACF,IAEN,EAAA,EAAA,KAAC,EAAD,CAAY,UAAU,mCACpB,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,0BAAf,CACG,EAAM,SAAW,IAChB,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,2DAAkD,sBAAyB,CAAA,CAE3F,EAAM,IAAK,IACV,EAAA,EAAA,KAAC,EAAD,CAEQ,OACD,MACL,UAAY,GAAO,EAAW,GAAM,EAAY,EAAW,GAAI,CAAG,EAAG,MAAM,IAAI,CAC/E,YAAe,EAAY,EAAK,CAChC,aAAgB,EAAa,EAAK,CAClC,SAAU,EAAK,SAAa,EAAa,EAAK,CAAG,IAAA,GACjD,CAPK,EAAK,GAOV,CACF,CACE,GACK,CAAA,CACT,GAIV,SAAS,EAAc,CACrB,OAAM,MAAK,YAAW,UAAS,WAAU,YAQxC,CACD,IAAM,EAAgB,EAAgB,EAAK,WAAa,EAAgB,KAClE,EAAc,EAAc,EAAK,SAAW,kDAElD,OACE,EAAA,EAAA,KAAC,EAAD,CAAM,UAAW,EAAK,UAAY,4BAA8B,aAC9D,EAAA,EAAA,KAAC,EAAD,CAAa,UAAU,gBACrB,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,kCAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,0BAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,kDAAf,EACE,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,wCAAgC,EAAK,MAAa,CAAA,EAClE,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAW,8BAA8B,aAAkB,EAAK,SAAiB,CAAA,EAC1G,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAW,8BAA8B,aAAgB,EAAK,OAAe,CAAA,CACrG,EAAK,aAAe,YACnB,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAU,uEAA8D,MAAW,CAAA,CAE1G,GACL,EAAK,OACJ,EAAA,EAAA,KAAC,IAAD,CAAG,UAAU,2DAAmD,EAAK,KAAS,CAAA,EAEhF,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,2EAAf,CACG,EAAK,QACJ,EAAA,EAAA,MAAC,OAAD,CAAM,MAAM,iBAAZ,EACE,EAAA,EAAA,KAAC,EAAD,CAAc,UAAU,sBAAwB,CAAA,CAAC,EAAU,EAAK,MAAM,CACjE,GAER,EAAK,SACJ,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,qBAAa,EAAK,OAAc,CAAA,EAElD,EAAA,EAAA,MAAC,OAAD,CAAM,MAAO,OAAO,EAAK,UAAU,UAAnC,EACE,EAAA,EAAA,KAAC,EAAD,CAAU,UAAU,sBAAwB,CAAA,CAAC,EAAQ,EAAK,EAAK,UAAU,CACpE,GACH,GACF,IACN,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,wCAAf,CACG,EAAK,YACJ,EAAA,EAAA,MAAC,EAAD,CAAQ,KAAK,KAAK,UAAU,2DAA2D,QAAS,WAAhG,EACE,EAAA,EAAA,KAAC,EAAD,CAAc,UAAU,mBAAqB,CAAA,CAAA,SACtC,GAEV,IACC,EAAA,EAAA,KAAC,EAAD,CAAQ,KAAK,KAAK,QAAQ,UAAU,UAAU,cAAc,QAAS,WAAU,SAEtE,CAAA,EAEX,EAAA,EAAA,MAAC,EAAD,CAAQ,KAAK,KAAK,QAAQ,UAAU,UAAU,oFAAoF,QAAS,WAA3I,EACE,EAAA,EAAA,KAAC,EAAD,CAAG,UAAU,mBAAqB,CAAA,CAAA,UAC3B,GACL,GACF,GACM,CAAA,CACT,CAAA"}
|
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
|
|
13
13
|
<link rel="manifest" href="manifest.webmanifest">
|
|
14
14
|
<link rel="apple-touch-icon" href="icons/agent-relay-192.png">
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-GoC6YrKn.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-C_JMBXql.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/lucide-react-BYZuAhQs.js">
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
<link rel="modulepreload" crossorigin href="./assets/badge-BLar6FjP.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="./assets/input-B_fUHtLD.js">
|
|
40
40
|
<link rel="modulepreload" crossorigin href="./assets/themes-DjSwmAZN.js">
|
|
41
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
41
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B95vV-dd.css">
|
|
42
42
|
</head>
|
|
43
43
|
<body class="bg-background text-foreground antialiased">
|
|
44
44
|
<div id="root"></div>
|
|
@@ -10,6 +10,7 @@ const PROVIDER_QUOTA_STALE_AFTER_MS = 10 * 60_000;
|
|
|
10
10
|
const QUOTA_UTILIZATION_EPSILON = 1e-9;
|
|
11
11
|
const QUOTA_RESET_BOUNDARY_ADVANCE_MS = 5_000;
|
|
12
12
|
const QUOTA_RESET_UTILIZATION_MAX = 0.01;
|
|
13
|
+
const FIVE_HOUR_MS = 5 * 60 * 60_000;
|
|
13
14
|
|
|
14
15
|
export interface ProviderQuotaAdvisoryInput {
|
|
15
16
|
provider: string;
|
|
@@ -258,11 +259,20 @@ function quotaWindowUtilizationRegressed(existing: QuotaState["windows"][number]
|
|
|
258
259
|
&& !quotaWindowLooksReset(existing, incoming);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
function publicQuotaState(quota: StoredQuotaState): QuotaState {
|
|
262
|
+
function publicQuotaState(quota: StoredQuotaState, now?: number): QuotaState {
|
|
263
|
+
let emulated = false;
|
|
264
|
+
const windows = quota.windows.map((window) => {
|
|
265
|
+
const period = quotaWindowPeriodMs(window.name);
|
|
266
|
+
if (now === undefined || period === undefined || typeof window.resetsAt !== "number" || !Number.isFinite(window.resetsAt) || now < window.resetsAt) return { ...window };
|
|
267
|
+
let resetsAt = window.resetsAt;
|
|
268
|
+
while (now >= resetsAt) resetsAt += period;
|
|
269
|
+
emulated = true;
|
|
270
|
+
return { ...window, utilization: 0, resetsAt };
|
|
271
|
+
});
|
|
262
272
|
return {
|
|
263
|
-
windows
|
|
264
|
-
source: quota.source,
|
|
265
|
-
updatedAt: quota.updatedAt,
|
|
273
|
+
windows,
|
|
274
|
+
source: emulated ? "emulated" : quota.source,
|
|
275
|
+
updatedAt: emulated && now !== undefined ? Math.max(quota.updatedAt, now) : quota.updatedAt,
|
|
266
276
|
};
|
|
267
277
|
}
|
|
268
278
|
|
|
@@ -287,7 +297,7 @@ function storedQuotaWindowSampleTimes(quota: StoredQuotaState): Map<string, numb
|
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
// Source priority for per-window precedence: "ingest" (WHAM/API) beats "probe" (agent self-report).
|
|
290
|
-
const QUOTA_SOURCE_PRIORITY: Record<string, number> = { ingest: 3, header: 2, probe: 1, statusline: 0 };
|
|
300
|
+
const QUOTA_SOURCE_PRIORITY: Record<string, number> = { ingest: 3, header: 2, probe: 1, statusline: 0, emulated: -1 };
|
|
291
301
|
function quotaSourcePriority(source: string): number {
|
|
292
302
|
return QUOTA_SOURCE_PRIORITY[source] ?? 1;
|
|
293
303
|
}
|
|
@@ -345,6 +355,12 @@ function quotaWindowSampleTimes(provider: string, accountKey: string, fallback?:
|
|
|
345
355
|
return times;
|
|
346
356
|
}
|
|
347
357
|
|
|
358
|
+
function quotaWindowPeriodMs(name: string): number | undefined {
|
|
359
|
+
if (name === "five_hour" || name === "primary") return FIVE_HOUR_MS;
|
|
360
|
+
if (name === "seven_day" || name === "seven_day_sonnet" || name === "seven_day_opus" || name === "secondary") return 7 * DAY_MS;
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
348
364
|
function mergeQuotaWindows(existing: StoredQuotaState | undefined, incoming: QuotaState, provider: string, accountKey: string): {
|
|
349
365
|
quota: StoredQuotaState;
|
|
350
366
|
acceptedIncomingWindow: boolean;
|
|
@@ -434,7 +450,7 @@ function rowToSnapshot(row: ProviderQuotaSnapshotRow): ProviderQuotaSnapshot {
|
|
|
434
450
|
function rowToRecord(row: ProviderQuotaRow, history: ProviderQuotaSnapshot[], now: number): ProviderQuotaRecord {
|
|
435
451
|
const lastError = parseJson<ProviderQuotaError | undefined>(row.last_error, undefined);
|
|
436
452
|
const lastUpdatedAt = row.last_updated_at ?? undefined;
|
|
437
|
-
const quota = row.quota_state ? publicQuotaState(parseStoredQuotaState(row.quota_state, { windows: [], source: "probe", updatedAt: lastUpdatedAt ?? row.last_attempt_at })) : undefined;
|
|
453
|
+
const quota = row.quota_state ? publicQuotaState(parseStoredQuotaState(row.quota_state, { windows: [], source: "probe", updatedAt: lastUpdatedAt ?? row.last_attempt_at }), now) : undefined;
|
|
438
454
|
const burn = quota ? deriveProviderQuotaBurn({
|
|
439
455
|
provider: row.provider,
|
|
440
456
|
quota,
|
|
@@ -26,7 +26,8 @@ export type NotificationType =
|
|
|
26
26
|
| "workspace.auto_abandoned"
|
|
27
27
|
| "workspace.review_request"
|
|
28
28
|
| "workspace.review_changes_requested"
|
|
29
|
-
| "routing.policy.changed"
|
|
29
|
+
| "routing.policy.changed"
|
|
30
|
+
| "routing.quota.band_transition";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* The entities a notification is ABOUT: routing inputs (#442) and explainability subjects (#446).
|
|
@@ -96,6 +97,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
|
|
|
96
97
|
"workspace.review_request": null,
|
|
97
98
|
"workspace.review_changes_requested": null,
|
|
98
99
|
"routing.policy.changed": 15 * MINUTES,
|
|
100
|
+
"routing.quota.band_transition": 30 * MINUTES,
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
export const NOTIFICATION_TYPES = Object.keys(DEFAULT_TTL_MS) as NotificationType[];
|
package/src/quota-advisory.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { routeNotification } from "./notification-router";
|
|
|
5
5
|
import { DEFAULT_QUOTA_THRESHOLD_CONFIG, validateQuotaThresholdConfig } from "./quota-threshold-config";
|
|
6
6
|
import { parseJson } from "./utils";
|
|
7
7
|
import { setProviderQuotaAdvisoryHandler, type ProviderQuotaAdvisoryInput } from "./db/provider-quotas.ts";
|
|
8
|
+
import { processBandTransitions } from "./quota-band-transition";
|
|
8
9
|
import type { QuotaState } from "./types";
|
|
9
10
|
|
|
10
11
|
type QuotaAdvisoryStage = "warn" | "critical" | "reset";
|
|
@@ -99,6 +100,7 @@ function handleProviderQuotaAdvisory(input: ProviderQuotaAdvisoryInput): string
|
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
processBandTransitions(input);
|
|
102
104
|
return JSON.stringify(nextState);
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// #738 — debounced band-transition notifications for quota routing bands.
|
|
2
|
+
//
|
|
3
|
+
// Fires a `routing.quota.band_transition` event when a provider crosses a
|
|
4
|
+
// quota-band boundary (normal → caution → avoid-large → hard-avoid, and
|
|
5
|
+
// back). Hysteresis: the new band must persist for DEBOUNCE_SAMPLES
|
|
6
|
+
// consecutive accepted-window samples before the notification fires, which
|
|
7
|
+
// prevents rapid flapping from bursty burn rates. Low-confidence windows
|
|
8
|
+
// (burnRatio: null) are skipped entirely.
|
|
9
|
+
//
|
|
10
|
+
// Called from handleProviderQuotaAdvisory in quota-advisory.ts so the check
|
|
11
|
+
// runs on the same event hook as the utilization-threshold advisory — no
|
|
12
|
+
// parallel delivery channel.
|
|
13
|
+
import { emitRelayEvent } from "./events";
|
|
14
|
+
import { routeNotification } from "./notification-router";
|
|
15
|
+
import { getSpawnCapableAgentIds } from "./routing-policy-push";
|
|
16
|
+
import { getProviderBandInfo } from "./spawn-targets";
|
|
17
|
+
import type { ProviderBandWindow, QuotaBand } from "./spawn-targets";
|
|
18
|
+
import type { ProviderQuotaAdvisoryInput } from "./db/provider-quotas";
|
|
19
|
+
|
|
20
|
+
const DEBOUNCE_SAMPLES = 3;
|
|
21
|
+
|
|
22
|
+
interface TransitionState {
|
|
23
|
+
lastNotifiedBand: QuotaBand;
|
|
24
|
+
pendingBand: QuotaBand | null;
|
|
25
|
+
pendingCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const bandState = new Map<string, TransitionState>();
|
|
29
|
+
|
|
30
|
+
export function _resetBandTransitionStateForTesting(): void {
|
|
31
|
+
bandState.clear();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function processBandTransitions(input: ProviderQuotaAdvisoryInput): void {
|
|
35
|
+
const info = getProviderBandInfo(input.provider, input.now);
|
|
36
|
+
if (!info) return;
|
|
37
|
+
|
|
38
|
+
for (const window of info.windows) {
|
|
39
|
+
if (window.confidence === "low" || window.burnRatio === null) continue;
|
|
40
|
+
|
|
41
|
+
const key = `${input.provider}:${window.role}`;
|
|
42
|
+
const state: TransitionState = bandState.get(key) ?? { lastNotifiedBand: "normal", pendingBand: null, pendingCount: 0 };
|
|
43
|
+
const currentBand = window.band;
|
|
44
|
+
|
|
45
|
+
if (currentBand === state.lastNotifiedBand) {
|
|
46
|
+
if (state.pendingBand !== null) {
|
|
47
|
+
bandState.set(key, { ...state, pendingBand: null, pendingCount: 0 });
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (currentBand === state.pendingBand) {
|
|
53
|
+
state.pendingCount += 1;
|
|
54
|
+
} else {
|
|
55
|
+
state.pendingBand = currentBand;
|
|
56
|
+
state.pendingCount = 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (state.pendingCount >= DEBOUNCE_SAMPLES) {
|
|
60
|
+
const previousBand = state.lastNotifiedBand;
|
|
61
|
+
state.lastNotifiedBand = currentBand;
|
|
62
|
+
state.pendingBand = null;
|
|
63
|
+
state.pendingCount = 0;
|
|
64
|
+
bandState.set(key, state);
|
|
65
|
+
emitBandTransition({
|
|
66
|
+
provider: input.provider,
|
|
67
|
+
window,
|
|
68
|
+
previousBand,
|
|
69
|
+
recommendedAction: info.recommendedAction,
|
|
70
|
+
now: input.now,
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
bandState.set(key, state);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function emitBandTransition(input: {
|
|
79
|
+
provider: string;
|
|
80
|
+
window: ProviderBandWindow;
|
|
81
|
+
previousBand: QuotaBand;
|
|
82
|
+
recommendedAction: string;
|
|
83
|
+
now: number;
|
|
84
|
+
}): void {
|
|
85
|
+
const { provider, window, previousBand, recommendedAction, now } = input;
|
|
86
|
+
const direction = bandRank(window.band) > bandRank(previousBand) ? "degraded" : "recovered";
|
|
87
|
+
const subject = `${provider}:${window.role}`;
|
|
88
|
+
const payload = {
|
|
89
|
+
kind: "routing.quota.band_transition",
|
|
90
|
+
provider,
|
|
91
|
+
window: window.role,
|
|
92
|
+
previousBand,
|
|
93
|
+
newBand: window.band,
|
|
94
|
+
direction,
|
|
95
|
+
utilization: window.utilization,
|
|
96
|
+
burnRatio: window.burnRatio,
|
|
97
|
+
projectedWallAt: window.projectedWallAt,
|
|
98
|
+
recommendedAction,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
emitRelayEvent({
|
|
102
|
+
type: "routing.quota.band_transition",
|
|
103
|
+
source: "server",
|
|
104
|
+
subject,
|
|
105
|
+
data: payload,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const recipients = getSpawnCapableAgentIds();
|
|
109
|
+
if (recipients.length === 0) return;
|
|
110
|
+
|
|
111
|
+
routeNotification({
|
|
112
|
+
type: "routing.quota.band_transition",
|
|
113
|
+
scope: {},
|
|
114
|
+
recipients,
|
|
115
|
+
message: {
|
|
116
|
+
subject: `${providerLabel(provider)} ${window.role} quota band → ${window.band}`,
|
|
117
|
+
body: buildBody(input),
|
|
118
|
+
payload,
|
|
119
|
+
replyExpected: false,
|
|
120
|
+
},
|
|
121
|
+
emittedAt: now,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildBody(input: {
|
|
126
|
+
provider: string;
|
|
127
|
+
window: ProviderBandWindow;
|
|
128
|
+
previousBand: QuotaBand;
|
|
129
|
+
recommendedAction: string;
|
|
130
|
+
}): string {
|
|
131
|
+
const { provider, window, previousBand, recommendedAction } = input;
|
|
132
|
+
const util = Math.round(window.utilization * 100);
|
|
133
|
+
const burn = window.burnRatio !== null ? ` burn×${window.burnRatio.toFixed(1)}` : "";
|
|
134
|
+
const proj = window.projectedWallAt !== null
|
|
135
|
+
? ` (wall ~${new Date(window.projectedWallAt).toISOString()})`
|
|
136
|
+
: "";
|
|
137
|
+
return `${providerLabel(provider)} ${window.role} quota band: ${previousBand} → ${window.band}. Utilization ${util}%${burn}${proj}. ${recommendedAction}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function bandRank(band: QuotaBand): number {
|
|
141
|
+
const ranks: Record<QuotaBand, number> = { "normal": 0, "caution": 1, "avoid-large": 2, "hard-avoid": 3 };
|
|
142
|
+
return ranks[band];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function providerLabel(provider: string): string {
|
|
146
|
+
return provider.slice(0, 1).toUpperCase() + provider.slice(1);
|
|
147
|
+
}
|
package/src/quota.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
2
|
import type { ProviderQuotaError, QuotaState, QuotaWindowName, QuotaSource, QuotaUnit } from "./types";
|
|
3
3
|
|
|
4
|
-
const QUOTA_SOURCES = new Set<QuotaSource>(["header", "probe", "ingest", "statusline"]);
|
|
4
|
+
const QUOTA_SOURCES = new Set<QuotaSource>(["header", "probe", "ingest", "statusline", "emulated"]);
|
|
5
5
|
const QUOTA_UNITS = new Set<QuotaUnit>(["tokens", "requests", "%"]);
|
|
6
6
|
const QUOTA_WINDOWS = new Set<QuotaWindowName>(["five_hour", "seven_day", "seven_day_sonnet", "seven_day_opus", "primary", "secondary"]);
|
|
7
7
|
|
package/src/routes/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { deleteMyAvatar, deleteUserAvatarById, getMe, getUserAvatar, getUsers, h
|
|
|
6
6
|
import { deleteArtifactById, getArtifactById, getArtifactContent, getArtifacts, headArtifactContent, postArtifact } from "./artifacts";
|
|
7
7
|
import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomations, patchAutomation, postAutomation, postAutomationRun } from "./automations";
|
|
8
8
|
import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
|
|
9
|
-
import { getProviderQuotasRoute, postProviderQuotaLeaseAcquireRoute, postProviderQuotaLeaseReleaseRoute, postProviderQuotaPublisherLeaseAcquireRoute, postProviderQuotaPublisherLeaseReleaseRoute, postProviderQuotaRoute } from "./provider-quotas";
|
|
9
|
+
import { getProviderQuotaAdvisoryRoute, getProviderQuotasRoute, postProviderQuotaLeaseAcquireRoute, postProviderQuotaLeaseReleaseRoute, postProviderQuotaPublisherLeaseAcquireRoute, postProviderQuotaPublisherLeaseReleaseRoute, postProviderQuotaRoute } from "./provider-quotas";
|
|
10
10
|
import { getProviderQuotaConfigRoute, getProviderQuotaConfigsRoute, putProviderQuotaConfigRoute } from "./provider-quota-config";
|
|
11
11
|
import { getRoutingPolicyRoute, putRoutingPolicyRoute } from "./routing-policy";
|
|
12
12
|
import { deleteProvisioningCapabilityRoute, getProvisioningCapabilitiesRoute, getProvisioningCapabilityRoute, patchProvisioningCapabilityRoute, patchProvisioningVariantRoute, postProvisioningCapabilityRoute, postProvisioningVariantImportRoute, postProvisioningVariantMcpRoute, postProvisioningVariantUpdateRoute, postProvisioningVariantValidateRoute } from "./provisioning-registry";
|
|
@@ -113,6 +113,7 @@ const routes: Route[] = [
|
|
|
113
113
|
route("GET", "/api/agents", getAgents),
|
|
114
114
|
route("GET", "/api/agents/find", findAgents),
|
|
115
115
|
route("POST", "/api/route", postRouteAdvice),
|
|
116
|
+
route("GET", "/api/provider-quotas/advisory", getProviderQuotaAdvisoryRoute),
|
|
116
117
|
route("GET", "/api/provider-quotas", getProviderQuotasRoute),
|
|
117
118
|
route("POST", "/api/provider-quotas", postProviderQuotaRoute),
|
|
118
119
|
route("POST", "/api/provider-quota-leases/acquire", postProviderQuotaPublisherLeaseAcquireRoute),
|
|
@@ -5,6 +5,7 @@ import { quotaStateFromUnknown, providerQuotaErrorFromUnknown } from "../quota";
|
|
|
5
5
|
import { authContextFromRequest } from "../services/auth-context";
|
|
6
6
|
import { cleanString } from "../validation";
|
|
7
7
|
import { authorizeRoute, error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
|
|
8
|
+
import { providerQuotaAdvisories } from "../spawn-targets";
|
|
8
9
|
import type { CodexResetCredits, ProviderQuotaUpdateInput } from "../types";
|
|
9
10
|
|
|
10
11
|
function cleanCodexResetCredits(value: unknown): CodexResetCredits | undefined {
|
|
@@ -61,6 +62,23 @@ export const getProviderQuotasRoute: Handler = (req) => {
|
|
|
61
62
|
}));
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
// #737 — per-provider quota burn advisory (band + penalty) for the dashboard quota widget.
|
|
66
|
+
// Factors the same quotaAdvisoryFor computation used by relay_spawn_targets so the
|
|
67
|
+
// dashboard reads the server-computed band without re-deriving routing-policy thresholds
|
|
68
|
+
// client-side. Only providers with active quota records appear in the map.
|
|
69
|
+
export const getProviderQuotaAdvisoryRoute: Handler = () => {
|
|
70
|
+
const advisoriesMap = providerQuotaAdvisories();
|
|
71
|
+
const advisories: Record<string, { quotaPenaltyBand: string; quotaPenalty: number; blockedNow: boolean }> = {};
|
|
72
|
+
for (const [provider, advisory] of advisoriesMap) {
|
|
73
|
+
advisories[provider] = {
|
|
74
|
+
quotaPenaltyBand: advisory.quotaPenaltyBand,
|
|
75
|
+
quotaPenalty: advisory.quotaPenalty,
|
|
76
|
+
blockedNow: advisory.blockedNow,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return json({ advisories });
|
|
80
|
+
};
|
|
81
|
+
|
|
64
82
|
export const postProviderQuotaRoute: Handler = async (req) => {
|
|
65
83
|
const parsed = await parseBody<unknown>(req);
|
|
66
84
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -52,7 +52,7 @@ export function notifyRoutingPolicyChanged(policy: RoutingPolicy, opts: { update
|
|
|
52
52
|
|
|
53
53
|
type AgentRow = { id: string; meta: string | null };
|
|
54
54
|
|
|
55
|
-
function getSpawnCapableAgentIds(): string[] {
|
|
55
|
+
export function getSpawnCapableAgentIds(): string[] {
|
|
56
56
|
const now = Date.now();
|
|
57
57
|
const candidates = getDb().query(`
|
|
58
58
|
SELECT id, meta FROM agents
|
package/src/security.ts
CHANGED
|
@@ -198,6 +198,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
198
198
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
199
199
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
200
200
|
if (pathname === "/api/provider-quota-leases/acquire" || pathname === "/api/provider-quota-leases/release") return "quota:write";
|
|
201
|
+
if (pathname === "/api/provider-quotas/advisory") return "agent:read";
|
|
201
202
|
if (pathname === "/api/provider-quotas") return method === "GET" ? "agent:read" : "quota:write";
|
|
202
203
|
if (pathname === "/api/routing-policy") return method === "GET" ? "agent:read" : "settings:write:routing";
|
|
203
204
|
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
@@ -283,6 +284,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
283
284
|
if (pathname === "/api/agents/spawn") return "command:spawn";
|
|
284
285
|
if (pathname.startsWith("/api/agents")) return method === "GET" ? "agent:read" : "agent:write";
|
|
285
286
|
if (pathname === "/api/provider-quota-leases/acquire" || pathname === "/api/provider-quota-leases/release") return "quota:write";
|
|
287
|
+
if (pathname === "/api/provider-quotas/advisory") return "agent:read";
|
|
286
288
|
if (pathname === "/api/provider-quotas") return method === "GET" ? "agent:read" : "quota:write";
|
|
287
289
|
// Per-provider quota config (#605): reads are agent:read (the orchestrator
|
|
288
290
|
// collector reads its config each tick); writes are settings:write:provider.
|
package/src/spawn-targets.ts
CHANGED
|
@@ -143,7 +143,7 @@ function providerMatrixFor(o: Orchestrator, quotaAdvisories: Map<SpawnProvider,
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function providerQuotaAdvisories(now = Date.now()): Map<SpawnProvider, SpawnQuotaAdvisory> {
|
|
146
|
+
export function providerQuotaAdvisories(now = Date.now()): Map<SpawnProvider, SpawnQuotaAdvisory> {
|
|
147
147
|
const policy = getRoutingPolicy();
|
|
148
148
|
const byProvider = new Map<SpawnProvider, ProviderQuotaRecord[]>();
|
|
149
149
|
for (const record of listProviderQuotas({ historyLimit: 0, now }).quotas) {
|
|
@@ -226,6 +226,46 @@ function spawnQuotaWindow(
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// === Public band-info API for quota-band-transition (#738) ===
|
|
230
|
+
|
|
231
|
+
export type QuotaBand = "normal" | "caution" | "avoid-large" | "hard-avoid";
|
|
232
|
+
|
|
233
|
+
export interface ProviderBandWindow {
|
|
234
|
+
role: ProviderQuotaBurnRole;
|
|
235
|
+
band: QuotaBand;
|
|
236
|
+
utilization: number;
|
|
237
|
+
burnRatio: number | null;
|
|
238
|
+
projectedWallAt: number | null;
|
|
239
|
+
confidence: "high" | "low";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface ProviderBandInfo {
|
|
243
|
+
windows: ProviderBandWindow[];
|
|
244
|
+
providerBand: QuotaBand;
|
|
245
|
+
recommendedAction: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Compute the current quota band advisory for a provider. Called by the band-transition
|
|
250
|
+
* notifier after each quota ingest to detect band crossings. Returns undefined if no
|
|
251
|
+
* quota records exist for the provider.
|
|
252
|
+
*/
|
|
253
|
+
export function getProviderBandInfo(provider: string, now: number): ProviderBandInfo | undefined {
|
|
254
|
+
const policy = getRoutingPolicy();
|
|
255
|
+
const records = listProviderQuotas({ provider, historyLimit: 0, now }).quotas;
|
|
256
|
+
if (records.length === 0) return undefined;
|
|
257
|
+
const advisory = quotaAdvisoryFor(records, now, policy.quotaBands, policy.providerQuota?.[provider as SpawnProvider]);
|
|
258
|
+
const windows: ProviderBandWindow[] = advisory.quotaBurn.windows.map((w) => ({
|
|
259
|
+
role: w.role,
|
|
260
|
+
band: quotaPenaltyBandFor(w.penalty, policy.quotaBands),
|
|
261
|
+
utilization: w.utilization,
|
|
262
|
+
burnRatio: w.burnRatio,
|
|
263
|
+
projectedWallAt: w.projectedWallAt,
|
|
264
|
+
confidence: w.confidence,
|
|
265
|
+
}));
|
|
266
|
+
return { windows, providerBand: advisory.quotaPenaltyBand, recommendedAction: advisory.recommendedAction };
|
|
267
|
+
}
|
|
268
|
+
|
|
229
269
|
function compareQuotaWindows(a: SpawnQuotaBurnWindow, b: SpawnQuotaBurnWindow): number {
|
|
230
270
|
if (a.blockedNow !== b.blockedNow) return a.blockedNow ? 1 : -1;
|
|
231
271
|
if (a.penalty !== b.penalty) return a.penalty - b.penalty;
|