agent-relay-server 0.77.0 → 0.78.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.
Files changed (65) hide show
  1. package/docs/openapi.json +79 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-Ct4RjObq.js → MessageBubble-Bkzw574C.js} +2 -2
  4. package/public/assets/{MessageBubble-Ct4RjObq.js.map → MessageBubble-Bkzw574C.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BQcLye6d.js → PlanGraphPanel-DaLJ5fKn.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BQcLye6d.js.map → PlanGraphPanel-DaLJ5fKn.js.map} +1 -1
  7. package/public/assets/{activity-DeQhQ17b.js → activity-BLFdfuxj.js} +2 -2
  8. package/public/assets/{activity-DeQhQ17b.js.map → activity-BLFdfuxj.js.map} +1 -1
  9. package/public/assets/{agents-coulRN38.js → agents-CEnKjxMD.js} +2 -2
  10. package/public/assets/{agents-coulRN38.js.map → agents-CEnKjxMD.js.map} +1 -1
  11. package/public/assets/{automation-V97mnoZp.js → automation-Crxi1JGO.js} +2 -2
  12. package/public/assets/{automation-V97mnoZp.js.map → automation-Crxi1JGO.js.map} +1 -1
  13. package/public/assets/{chat-B8fs8r1x.js → chat-CWmxVsVA.js} +2 -2
  14. package/public/assets/{chat-B8fs8r1x.js.map → chat-CWmxVsVA.js.map} +1 -1
  15. package/public/assets/{formatted-body-impl-CQ-8HQ7u.js → formatted-body-impl-CCA5xER4.js} +2 -2
  16. package/public/assets/{formatted-body-impl-CQ-8HQ7u.js.map → formatted-body-impl-CCA5xER4.js.map} +1 -1
  17. package/public/assets/{index-DJtda8l2.js → index-Du_Njun0.js} +5 -5
  18. package/public/assets/index-Du_Njun0.js.map +1 -0
  19. package/public/assets/{insights-CtT5cEif.js → insights-Dy4k6fpi.js} +2 -2
  20. package/public/assets/{insights-CtT5cEif.js.map → insights-Dy4k6fpi.js.map} +1 -1
  21. package/public/assets/{maintenance-BWLMjX8h.js → maintenance-Bgaxr-3J.js} +2 -2
  22. package/public/assets/{maintenance-BWLMjX8h.js.map → maintenance-Bgaxr-3J.js.map} +1 -1
  23. package/public/assets/{managed-agents-KxHV0VcQ.js → managed-agents-DKD0Ovct.js} +2 -2
  24. package/public/assets/{managed-agents-KxHV0VcQ.js.map → managed-agents-DKD0Ovct.js.map} +1 -1
  25. package/public/assets/{markdown-preview-impl-C5b5cBM0.js → markdown-preview-impl-OM7F_OtZ.js} +2 -2
  26. package/public/assets/{markdown-preview-impl-C5b5cBM0.js.map → markdown-preview-impl-OM7F_OtZ.js.map} +1 -1
  27. package/public/assets/{memory-Bh3UgQCk.js → memory-BSvQoXx6.js} +2 -2
  28. package/public/assets/{memory-Bh3UgQCk.js.map → memory-BSvQoXx6.js.map} +1 -1
  29. package/public/assets/{messages-Dg7TSxpZ.js → messages-D-WqWrGU.js} +2 -2
  30. package/public/assets/{messages-Dg7TSxpZ.js.map → messages-D-WqWrGU.js.map} +1 -1
  31. package/public/assets/{orchestrators-BBKW7bO0.js → orchestrators-DZrmTmOK.js} +2 -2
  32. package/public/assets/{orchestrators-BBKW7bO0.js.map → orchestrators-DZrmTmOK.js.map} +1 -1
  33. package/public/assets/{overview-a8lG62AZ.js → overview-C8Q92ay4.js} +2 -2
  34. package/public/assets/{overview-a8lG62AZ.js.map → overview-C8Q92ay4.js.map} +1 -1
  35. package/public/assets/{pairs-ChSYXFwu.js → pairs-B_3Q2kaN.js} +2 -2
  36. package/public/assets/{pairs-ChSYXFwu.js.map → pairs-B_3Q2kaN.js.map} +1 -1
  37. package/public/assets/{security-D0obEjDT.js → security-CUiGx0a5.js} +2 -2
  38. package/public/assets/{security-D0obEjDT.js.map → security-CUiGx0a5.js.map} +1 -1
  39. package/public/assets/{select-DwCP2Jhh.js → select-Bh9XLp7J.js} +2 -2
  40. package/public/assets/{select-DwCP2Jhh.js.map → select-Bh9XLp7J.js.map} +1 -1
  41. package/public/assets/{settings-f1l2R0e8.js → settings-Dox7P4g0.js} +2 -2
  42. package/public/assets/{settings-f1l2R0e8.js.map → settings-Dox7P4g0.js.map} +1 -1
  43. package/public/assets/{tasks-DDVfmUst.js → tasks-BIP2A8PU.js} +2 -2
  44. package/public/assets/{tasks-DDVfmUst.js.map → tasks-BIP2A8PU.js.map} +1 -1
  45. package/public/assets/{teams-COZKriLe.js → teams-pbVU7Y_N.js} +2 -2
  46. package/public/assets/{teams-COZKriLe.js.map → teams-pbVU7Y_N.js.map} +1 -1
  47. package/public/assets/{terminal-viewer-impl-CjOZ__aw.js → terminal-viewer-impl-M_3TITNG.js} +2 -2
  48. package/public/assets/{terminal-viewer-impl-CjOZ__aw.js.map → terminal-viewer-impl-M_3TITNG.js.map} +1 -1
  49. package/public/assets/{work-queue-BC7heUfD.js → work-queue-D12yWpsi.js} +2 -2
  50. package/public/assets/{work-queue-BC7heUfD.js.map → work-queue-D12yWpsi.js.map} +1 -1
  51. package/public/index.html +1 -1
  52. package/src/config-store.ts +3 -1
  53. package/src/db/index.ts +1 -0
  54. package/src/db/migrations.ts +21 -0
  55. package/src/db/provider-quota-leases.ts +134 -0
  56. package/src/db/provider-quotas.ts +179 -1
  57. package/src/db/schema.ts +13 -0
  58. package/src/index.ts +2 -0
  59. package/src/notification-types.ts +2 -0
  60. package/src/quota-advisory.ts +300 -0
  61. package/src/quota-threshold-config.ts +46 -0
  62. package/src/routes/index.ts +3 -1
  63. package/src/routes/provider-quotas.ts +56 -2
  64. package/src/token-db.ts +1 -1
  65. package/public/assets/index-DJtda8l2.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{Vn as e,_n as t,i as n,lt as r,on as i}from"./lucide-react-BH2ROV8n.js";import{l as a,t as o}from"./store-m74m5eNF.js";import{U as s,d as c,ut as l}from"./display-DHgdn4vN.js";import{t as u}from"./badge-BxG6_yfp.js";import{t as d}from"./button-D5k3GbF6.js";import{B as f,K as p,s as m}from"./index-DJtda8l2.js";import{r as h,t as g}from"./card-DKyFtrez.js";var _=e(),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),i=o(e=>e.set),s=o(e=>e.doClaimTask),l=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&&l(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)(r,{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=>i({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(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]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:r,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.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:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.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`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(r,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.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)(i,{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:c,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-BC7heUfD.js.map
1
+ import{Vn as e,_n as t,i as n,lt as r,on as i}from"./lucide-react-BH2ROV8n.js";import{l as a,t as o}from"./store-m74m5eNF.js";import{U as s,d as c,ut as l}from"./display-DHgdn4vN.js";import{t as u}from"./badge-BxG6_yfp.js";import{t as d}from"./button-D5k3GbF6.js";import{B as f,K as p,s as m}from"./index-Du_Njun0.js";import{r as h,t as g}from"./card-DKyFtrez.js";var _=e(),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),i=o(e=>e.set),s=o(e=>e.doClaimTask),l=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&&l(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)(r,{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=>i({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(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]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:r,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.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:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.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`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(r,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.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)(i,{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:c,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-D12yWpsi.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-BC7heUfD.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-D12yWpsi.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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"}
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-DJtda8l2.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-Du_Njun0.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-CP_edrrL.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-BH2ROV8n.js">
@@ -1,6 +1,7 @@
1
1
  import { getDb, ValidationError } from "./db";
2
2
  import { cleanEnum, cleanString, cleanStringArray } from "./validation";
3
3
  import { DEFAULT_CONTEXT_THRESHOLD_CONFIG, validateContextThresholdConfig } from "./context-threshold-config";
4
+ import { DEFAULT_QUOTA_THRESHOLD_CONFIG, validateQuotaThresholdConfig } from "./quota-threshold-config";
4
5
  import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-resume-config";
5
6
  import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
6
7
  import { PROJECT_INSTRUCTION_POLICY_NAMES } from "./project-instructions";
@@ -499,7 +500,7 @@ function validateInsightsConfig(value: unknown): InsightsConfig {
499
500
  // Relay-driven lifecycle push notifications (#239 event bus). Default-on; the
500
501
  // operator can flip the master switch or individual events off via the generic
501
502
  // config route. Push messages wake recipients, so they must be suppressible.
502
- const NOTIFICATIONS_CONFIG_DEFAULTS: NotificationsConfig = { enabled: true, branchLanded: true, agentReady: true, agentExited: true, agentSpawnFailed: true, contextThreshold: DEFAULT_CONTEXT_THRESHOLD_CONFIG };
503
+ const NOTIFICATIONS_CONFIG_DEFAULTS: NotificationsConfig = { enabled: true, branchLanded: true, agentReady: true, agentExited: true, agentSpawnFailed: true, contextThreshold: DEFAULT_CONTEXT_THRESHOLD_CONFIG, quotaThreshold: DEFAULT_QUOTA_THRESHOLD_CONFIG };
503
504
 
504
505
  function validateNotificationsConfig(value: unknown): NotificationsConfig {
505
506
  if (!isRecord(value)) throw new ValidationError("notifications config value must be an object");
@@ -512,6 +513,7 @@ function validateNotificationsConfig(value: unknown): NotificationsConfig {
512
513
  agentExited: bool("agentExited"),
513
514
  agentSpawnFailed: bool("agentSpawnFailed"),
514
515
  contextThreshold: validateContextThresholdConfig(value.contextThreshold),
516
+ quotaThreshold: validateQuotaThresholdConfig(value.quotaThreshold),
515
517
  };
516
518
  }
517
519
 
package/src/db/index.ts CHANGED
@@ -30,3 +30,4 @@ export * from "./projects.ts";
30
30
  export * from "./project-entries.ts";
31
31
  export * from "./provisioning-registry.ts";
32
32
  export * from "./provider-quotas.ts";
33
+ export * from "./provider-quota-leases.ts";
@@ -17,6 +17,7 @@ import { matchAgents } from "../agent-ref";
17
17
  import { getDb } from "./connection.ts";
18
18
  import { ensureContinuationArchiveSearchSchema } from "./continuation-archives.ts";
19
19
  import { normalizeReactionEmoji } from "./mappers.ts";
20
+ import { normalizeProviderQuotaAccountKeys } from "./provider-quotas.ts";
20
21
  import { projectRootsMissingIssueRepo, setProjectIssueRepo } from "./projects.ts";
21
22
  import { repoFromGitRemote } from "../services/git-remote.ts";
22
23
  import type {
@@ -657,6 +658,7 @@ export function applyMigrations(): void {
657
658
  account_key TEXT NOT NULL,
658
659
  quota_state TEXT,
659
660
  source_agent_ids TEXT NOT NULL DEFAULT '[]',
661
+ quota_advisory_state TEXT NOT NULL DEFAULT '{}',
660
662
  last_updated_at INTEGER,
661
663
  last_attempt_at INTEGER NOT NULL,
662
664
  last_error TEXT,
@@ -665,6 +667,10 @@ export function applyMigrations(): void {
665
667
  PRIMARY KEY (provider, account_key)
666
668
  )
667
669
  `);
670
+ const providerQuotaCols = (getDb().query("PRAGMA table_info(provider_quotas)").all() as Array<{ name: string }>).map((col) => col.name);
671
+ if (!providerQuotaCols.includes("quota_advisory_state")) {
672
+ getDb().run("ALTER TABLE provider_quotas ADD COLUMN quota_advisory_state TEXT NOT NULL DEFAULT '{}'");
673
+ }
668
674
  getDb().run(`
669
675
  INSERT INTO provider_quotas (
670
676
  provider, account_key, quota_state, source_agent_ids, last_updated_at,
@@ -700,6 +706,21 @@ export function applyMigrations(): void {
700
706
  `);
701
707
  getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_account_time ON quota_snapshots(provider, account_key, captured_at DESC)");
702
708
  getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC)");
709
+ normalizeProviderQuotaAccountKeys();
710
+
711
+ getDb().run(`
712
+ CREATE TABLE IF NOT EXISTS provider_quota_leases (
713
+ provider TEXT NOT NULL,
714
+ account_key TEXT NOT NULL,
715
+ orchestrator_id TEXT NOT NULL,
716
+ lease_token TEXT NOT NULL,
717
+ expires_at INTEGER NOT NULL,
718
+ updated_at INTEGER NOT NULL,
719
+ created_at INTEGER NOT NULL,
720
+ PRIMARY KEY (provider, account_key)
721
+ )
722
+ `);
723
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_provider_quota_leases_expires ON provider_quota_leases(expires_at)");
703
724
 
704
725
  getDb().run(`
705
726
  CREATE TABLE IF NOT EXISTS memories (
@@ -0,0 +1,134 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDb } from "./connection.ts";
3
+
4
+ export interface ProviderQuotaLease {
5
+ provider: string;
6
+ accountKey: string;
7
+ orchestratorId: string;
8
+ leaseToken: string;
9
+ expiresAt: number;
10
+ updatedAt: number;
11
+ createdAt: number;
12
+ }
13
+
14
+ export interface ProviderQuotaLeaseAcquireResult {
15
+ acquired: boolean;
16
+ lease?: ProviderQuotaLease;
17
+ holder?: {
18
+ orchestratorId: string;
19
+ expiresAt: number;
20
+ };
21
+ retryAfterMs?: number;
22
+ }
23
+
24
+ type ProviderQuotaLeaseRow = {
25
+ provider: string;
26
+ account_key: string;
27
+ orchestrator_id: string;
28
+ lease_token: string;
29
+ expires_at: number;
30
+ updated_at: number;
31
+ created_at: number;
32
+ };
33
+
34
+ function normalizeProvider(value: string): string {
35
+ return value.trim().toLowerCase();
36
+ }
37
+
38
+ function cleanAccountKey(value: string): string {
39
+ return value.trim();
40
+ }
41
+
42
+ function rowToLease(row: ProviderQuotaLeaseRow): ProviderQuotaLease {
43
+ return {
44
+ provider: row.provider,
45
+ accountKey: row.account_key,
46
+ orchestratorId: row.orchestrator_id,
47
+ leaseToken: row.lease_token,
48
+ expiresAt: row.expires_at,
49
+ updatedAt: row.updated_at,
50
+ createdAt: row.created_at,
51
+ };
52
+ }
53
+
54
+ export function acquireProviderQuotaLease(input: {
55
+ provider: string;
56
+ accountKey: string;
57
+ orchestratorId: string;
58
+ leaseToken?: string;
59
+ ttlMs: number;
60
+ now?: number;
61
+ }): ProviderQuotaLeaseAcquireResult {
62
+ const provider = normalizeProvider(input.provider);
63
+ const accountKey = cleanAccountKey(input.accountKey);
64
+ const orchestratorId = input.orchestratorId.trim();
65
+ const ttlMs = Math.max(5_000, input.ttlMs);
66
+ const now = input.now ?? Date.now();
67
+ if (!provider) throw new Error("provider required");
68
+ if (!accountKey) throw new Error("accountKey required");
69
+ if (!orchestratorId) throw new Error("orchestratorId required");
70
+
71
+ const acquire = getDb().transaction(() => {
72
+ const existing = getDb().query("SELECT * FROM provider_quota_leases WHERE provider = ? AND account_key = ?")
73
+ .get(provider, accountKey) as ProviderQuotaLeaseRow | undefined;
74
+ const expired = !existing || existing.expires_at <= now;
75
+ const sameHolder = existing?.orchestrator_id === orchestratorId && existing.lease_token === input.leaseToken;
76
+ if (existing && !expired && !sameHolder) {
77
+ return {
78
+ acquired: false,
79
+ holder: { orchestratorId: existing.orchestrator_id, expiresAt: existing.expires_at },
80
+ retryAfterMs: Math.max(1_000, existing.expires_at - now),
81
+ } satisfies ProviderQuotaLeaseAcquireResult;
82
+ }
83
+
84
+ const leaseToken = sameHolder ? existing!.lease_token : randomUUID();
85
+ const createdAt = sameHolder ? existing!.created_at : now;
86
+ const expiresAt = now + ttlMs;
87
+ getDb().query(`
88
+ INSERT INTO provider_quota_leases (
89
+ provider, account_key, orchestrator_id, lease_token, expires_at, updated_at, created_at
90
+ )
91
+ VALUES (?, ?, ?, ?, ?, ?, ?)
92
+ ON CONFLICT(provider, account_key) DO UPDATE SET
93
+ orchestrator_id = excluded.orchestrator_id,
94
+ lease_token = excluded.lease_token,
95
+ expires_at = excluded.expires_at,
96
+ updated_at = excluded.updated_at
97
+ `).run(provider, accountKey, orchestratorId, leaseToken, expiresAt, now, createdAt);
98
+ return {
99
+ acquired: true,
100
+ lease: {
101
+ provider,
102
+ accountKey,
103
+ orchestratorId,
104
+ leaseToken,
105
+ expiresAt,
106
+ updatedAt: now,
107
+ createdAt,
108
+ },
109
+ } satisfies ProviderQuotaLeaseAcquireResult;
110
+ });
111
+
112
+ return acquire();
113
+ }
114
+
115
+ export function releaseProviderQuotaLease(input: {
116
+ provider: string;
117
+ accountKey: string;
118
+ orchestratorId: string;
119
+ leaseToken: string;
120
+ }): boolean {
121
+ const provider = normalizeProvider(input.provider);
122
+ const accountKey = cleanAccountKey(input.accountKey);
123
+ const result = getDb().query(`
124
+ DELETE FROM provider_quota_leases
125
+ WHERE provider = ? AND account_key = ? AND orchestrator_id = ? AND lease_token = ?
126
+ `).run(provider, accountKey, input.orchestratorId.trim(), input.leaseToken);
127
+ return result.changes > 0;
128
+ }
129
+
130
+ export function getProviderQuotaLease(provider: string, accountKey: string): ProviderQuotaLease | null {
131
+ const row = getDb().query("SELECT * FROM provider_quota_leases WHERE provider = ? AND account_key = ?")
132
+ .get(normalizeProvider(provider), cleanAccountKey(accountKey)) as ProviderQuotaLeaseRow | undefined;
133
+ return row ? rowToLease(row) : null;
134
+ }
@@ -6,14 +6,33 @@ import type { AgentCard, ProviderQuotaError, ProviderQuotaMatrix, ProviderQuotaR
6
6
 
7
7
  const PROVIDER_QUOTA_STALE_AFTER_MS = 10 * 60_000;
8
8
 
9
+ export interface ProviderQuotaAdvisoryInput {
10
+ provider: string;
11
+ accountKey: string;
12
+ previousQuota?: QuotaState;
13
+ quota: QuotaState;
14
+ sourceAgentIds: string[];
15
+ previousAdvisoryState?: string | null;
16
+ now: number;
17
+ }
18
+
19
+ let providerQuotaAdvisoryHandler: ((input: ProviderQuotaAdvisoryInput) => string) | undefined;
20
+
21
+ export function setProviderQuotaAdvisoryHandler(handler: ((input: ProviderQuotaAdvisoryInput) => string) | undefined): void {
22
+ providerQuotaAdvisoryHandler = handler;
23
+ }
24
+
9
25
  type ProviderQuotaRow = {
10
26
  provider: string;
11
27
  account_key: string;
12
28
  quota_state?: string | null;
13
29
  source_agent_ids?: string | null;
30
+ quota_advisory_state?: string | null;
14
31
  last_updated_at?: number | null;
15
32
  last_attempt_at: number;
16
33
  last_error?: string | null;
34
+ updated_at: number;
35
+ created_at: number;
17
36
  };
18
37
 
19
38
  type ProviderQuotaSnapshotRow = {
@@ -32,10 +51,126 @@ function normalizeProvider(value: string): string {
32
51
  return cleanKey(value).toLowerCase();
33
52
  }
34
53
 
54
+ function canonicalProviderQuotaAccountKey(provider: string, value: string): string {
55
+ const accountKey = cleanKey(value);
56
+ const legacySuffix = `:${normalizeProvider(provider)}`;
57
+ return accountKey.startsWith("host:") && accountKey.endsWith(legacySuffix)
58
+ ? accountKey.slice(0, -legacySuffix.length)
59
+ : accountKey;
60
+ }
61
+
62
+ function sourceAgentHost(sourceAgentId: string | undefined): string | undefined {
63
+ if (!sourceAgentId) return undefined;
64
+ const row = getDb().query("SELECT machine, rig FROM agents WHERE id = ?").get(sourceAgentId) as { machine?: string | null; rig?: string | null } | undefined;
65
+ const host = row?.machine ?? row?.rig ?? undefined;
66
+ return host?.trim() || undefined;
67
+ }
68
+
69
+ function hostFromAccountKey(accountKey: string): string | undefined {
70
+ if (!accountKey.startsWith("host:")) return undefined;
71
+ const host = accountKey.slice("host:".length);
72
+ return host && !host.includes(":") ? host : undefined;
73
+ }
74
+
35
75
  function normalizeSourceAgentIds(value: unknown): string[] {
36
76
  return [...new Set(parseStringArray(typeof value === "string" ? value : JSON.stringify(value ?? [])).filter(Boolean))];
37
77
  }
38
78
 
79
+ function quotaRowsForProvider(provider: string): ProviderQuotaRow[] {
80
+ return getDb().query("SELECT * FROM provider_quotas WHERE provider = ?").all(provider) as ProviderQuotaRow[];
81
+ }
82
+
83
+ function supersededAccountKeys(provider: string, accountKey: string, rawAccountKey: string, sourceAgentId: string | undefined): string[] {
84
+ const candidates = new Set<string>();
85
+ if (rawAccountKey && rawAccountKey !== accountKey) candidates.add(rawAccountKey);
86
+ const host = hostFromAccountKey(accountKey) ?? sourceAgentHost(sourceAgentId);
87
+ if (host) {
88
+ candidates.add(`host:${host}:${provider}`);
89
+ if (accountKey !== `host:${host}`) candidates.add(`host:${host}`);
90
+ }
91
+ if (sourceAgentId) {
92
+ for (const row of quotaRowsForProvider(provider)) {
93
+ if (row.account_key === accountKey) continue;
94
+ if (!normalizeSourceAgentIds(row.source_agent_ids).includes(sourceAgentId)) continue;
95
+ candidates.add(row.account_key);
96
+ }
97
+ }
98
+ candidates.delete(accountKey);
99
+ return [...candidates];
100
+ }
101
+
102
+ function newestQuotaState(rows: ProviderQuotaRow[]): string | null {
103
+ const row = rows
104
+ .filter((candidate) => candidate.quota_state)
105
+ .sort((a, b) => (b.last_updated_at ?? 0) - (a.last_updated_at ?? 0))[0];
106
+ return row?.quota_state ?? null;
107
+ }
108
+
109
+ function newestLastError(rows: ProviderQuotaRow[]): string | null {
110
+ const row = rows
111
+ .filter((candidate) => candidate.last_error)
112
+ .sort((a, b) => b.last_attempt_at - a.last_attempt_at)[0];
113
+ return row?.last_error ?? null;
114
+ }
115
+
116
+ function newestQuotaAdvisoryState(rows: ProviderQuotaRow[]): string {
117
+ const row = rows
118
+ .filter((candidate) => candidate.quota_advisory_state)
119
+ .sort((a, b) => b.updated_at - a.updated_at)[0];
120
+ return row?.quota_advisory_state ?? "{}";
121
+ }
122
+
123
+ function mergeProviderQuotaRows(provider: string, accountKey: string, oldAccountKeys: string[], now: number): number {
124
+ const keys = [...new Set(oldAccountKeys.filter((key) => key && key !== accountKey))];
125
+ if (keys.length === 0) return 0;
126
+ const placeholders = [accountKey, ...keys].map(() => "?").join(", ");
127
+ const rows = getDb().query(`SELECT * FROM provider_quotas WHERE provider = ? AND account_key IN (${placeholders})`).all(provider, accountKey, ...keys) as ProviderQuotaRow[];
128
+ const oldRows = rows.filter((row) => row.account_key !== accountKey);
129
+ if (oldRows.length === 0) return 0;
130
+
131
+ const sourceAgentIds = [...new Set(rows.flatMap((row) => normalizeSourceAgentIds(row.source_agent_ids)))];
132
+ const quotaState = newestQuotaState(rows);
133
+ const lastUpdatedAt = rows.reduce<number | undefined>((latest, row) => {
134
+ if (row.last_updated_at === null || row.last_updated_at === undefined) return latest;
135
+ return latest === undefined ? row.last_updated_at : Math.max(latest, row.last_updated_at);
136
+ }, undefined);
137
+ const lastAttemptAt = rows.reduce((latest, row) => Math.max(latest, row.last_attempt_at), 0);
138
+ const updatedAt = Math.max(now, ...rows.map((row) => row.updated_at));
139
+ const createdAt = Math.min(...rows.map((row) => row.created_at));
140
+
141
+ getDb().query(`
142
+ INSERT INTO provider_quotas (
143
+ provider, account_key, quota_state, source_agent_ids, last_updated_at,
144
+ last_attempt_at, last_error, quota_advisory_state, updated_at, created_at
145
+ )
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
147
+ ON CONFLICT(provider, account_key) DO UPDATE SET
148
+ quota_state = excluded.quota_state,
149
+ source_agent_ids = excluded.source_agent_ids,
150
+ last_updated_at = excluded.last_updated_at,
151
+ last_attempt_at = excluded.last_attempt_at,
152
+ last_error = excluded.last_error,
153
+ quota_advisory_state = excluded.quota_advisory_state,
154
+ updated_at = excluded.updated_at
155
+ `).run(
156
+ provider,
157
+ accountKey,
158
+ quotaState,
159
+ JSON.stringify(sourceAgentIds),
160
+ lastUpdatedAt ?? null,
161
+ lastAttemptAt || now,
162
+ newestLastError(rows),
163
+ newestQuotaAdvisoryState(rows),
164
+ updatedAt,
165
+ createdAt,
166
+ );
167
+
168
+ const oldPlaceholders = keys.map(() => "?").join(", ");
169
+ getDb().query(`UPDATE quota_snapshots SET account_key = ? WHERE provider = ? AND account_key IN (${oldPlaceholders})`).run(accountKey, provider, ...keys);
170
+ const result = getDb().query(`DELETE FROM provider_quotas WHERE provider = ? AND account_key IN (${oldPlaceholders})`).run(provider, ...keys);
171
+ return result.changes;
172
+ }
173
+
39
174
  function quotaFingerprint(quota: QuotaState): string {
40
175
  return JSON.stringify(quota.windows.map((window) => ({
41
176
  name: window.name,
@@ -99,9 +234,11 @@ function providerQuotaIdentityForAgent(agent: Pick<AgentCard, "id" | "name" | "t
99
234
 
100
235
  export function upsertProviderQuota(input: ProviderQuotaUpdateInput, now = Date.now()): ProviderQuotaRecord {
101
236
  const provider = normalizeProvider(input.provider);
102
- const accountKey = cleanKey(input.accountKey);
237
+ const rawAccountKey = cleanKey(input.accountKey);
238
+ const accountKey = canonicalProviderQuotaAccountKey(provider, rawAccountKey);
103
239
  if (!provider) throw new Error("provider required");
104
240
  if (!accountKey) throw new Error("accountKey required");
241
+ mergeProviderQuotaRows(provider, accountKey, supersededAccountKeys(provider, accountKey, rawAccountKey, input.sourceAgentId), now);
105
242
  const existing = getDb().query("SELECT * FROM provider_quotas WHERE provider = ? AND account_key = ?").get(provider, accountKey) as ProviderQuotaRow | undefined;
106
243
  const sourceAgentIds = normalizeSourceAgentIds(existing?.source_agent_ids);
107
244
  if (input.sourceAgentId && !sourceAgentIds.includes(input.sourceAgentId)) sourceAgentIds.push(input.sourceAgentId);
@@ -140,6 +277,21 @@ export function upsertProviderQuota(input: ProviderQuotaUpdateInput, now = Date.
140
277
  if (input.quota && latestSnapshotFingerprint(provider, accountKey) !== quotaFingerprint(input.quota)) {
141
278
  recordProviderQuotaSnapshot({ provider, accountKey, quota: input.quota, sourceAgentId: input.sourceAgentId }, now);
142
279
  }
280
+ if (input.quota) {
281
+ const quotaAdvisoryState = providerQuotaAdvisoryHandler?.({
282
+ provider,
283
+ accountKey,
284
+ previousQuota: existing?.quota_state ? parseJson<QuotaState>(existing.quota_state, { windows: [], source: "probe", updatedAt: 0 }) : undefined,
285
+ quota: input.quota,
286
+ sourceAgentIds,
287
+ previousAdvisoryState: existing?.quota_advisory_state,
288
+ now,
289
+ });
290
+ if (quotaAdvisoryState !== undefined) {
291
+ getDb().query("UPDATE provider_quotas SET quota_advisory_state = ? WHERE provider = ? AND account_key = ?").run(quotaAdvisoryState, provider, accountKey);
292
+ }
293
+ }
294
+ pruneProviderQuotaRecords(provider, undefined, now - DAY_MS);
143
295
 
144
296
  return listProviderQuotas({ provider, accountKey, historyLimit: 24, now }).quotas[0]!;
145
297
  }
@@ -170,6 +322,32 @@ function pruneProviderQuotaSnapshots(provider?: string, accountKey?: string, old
170
322
  return result.changes;
171
323
  }
172
324
 
325
+ function pruneProviderQuotaRecords(provider?: string, accountKey?: string, olderThan = Date.now() - DAY_MS): number {
326
+ const result = provider && accountKey
327
+ ? getDb().query("DELETE FROM provider_quotas WHERE provider = ? AND account_key = ? AND updated_at < ?").run(provider, accountKey, olderThan)
328
+ : provider
329
+ ? getDb().query("DELETE FROM provider_quotas WHERE provider = ? AND updated_at < ?").run(provider, olderThan)
330
+ : getDb().query("DELETE FROM provider_quotas WHERE updated_at < ?").run(olderThan);
331
+ return result.changes;
332
+ }
333
+
334
+ export function normalizeProviderQuotaAccountKeys(now = Date.now()): number {
335
+ let changes = 0;
336
+ for (const row of getDb().query("SELECT * FROM provider_quotas").all() as ProviderQuotaRow[]) {
337
+ let accountKey = canonicalProviderQuotaAccountKey(row.provider, row.account_key);
338
+ if (accountKey.startsWith("credential:")) {
339
+ const sourceAgentId = normalizeSourceAgentIds(row.source_agent_ids)[0];
340
+ const host = sourceAgentHost(sourceAgentId);
341
+ if (host) accountKey = `host:${host}`;
342
+ }
343
+ if (accountKey !== row.account_key) {
344
+ changes += mergeProviderQuotaRows(row.provider, accountKey, [row.account_key], now);
345
+ }
346
+ }
347
+ changes += pruneProviderQuotaRecords(undefined, undefined, now - DAY_MS);
348
+ return changes;
349
+ }
350
+
173
351
  function listProviderQuotaSnapshots(provider: string, accountKey: string, options: { since?: number; limit?: number } = {}): ProviderQuotaSnapshot[] {
174
352
  const limit = Math.max(1, Math.min(options.limit ?? 288, 1000));
175
353
  const params: any[] = [normalizeProvider(provider), cleanKey(accountKey)];
package/src/db/schema.ts CHANGED
@@ -307,6 +307,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
307
307
  account_key TEXT NOT NULL,
308
308
  quota_state TEXT,
309
309
  source_agent_ids TEXT NOT NULL DEFAULT '[]',
310
+ quota_advisory_state TEXT NOT NULL DEFAULT '{}',
310
311
  last_updated_at INTEGER,
311
312
  last_attempt_at INTEGER NOT NULL,
312
313
  last_error TEXT,
@@ -315,6 +316,18 @@ export function initDb(path: string = "agent-relay.db"): Database {
315
316
  PRIMARY KEY (provider, account_key)
316
317
  );
317
318
 
319
+ CREATE TABLE IF NOT EXISTS provider_quota_leases (
320
+ provider TEXT NOT NULL,
321
+ account_key TEXT NOT NULL,
322
+ orchestrator_id TEXT NOT NULL,
323
+ lease_token TEXT NOT NULL,
324
+ expires_at INTEGER NOT NULL,
325
+ updated_at INTEGER NOT NULL,
326
+ created_at INTEGER NOT NULL,
327
+ PRIMARY KEY (provider, account_key)
328
+ );
329
+ CREATE INDEX IF NOT EXISTS idx_provider_quota_leases_expires ON provider_quota_leases(expires_at);
330
+
318
331
  CREATE TABLE IF NOT EXISTS agent_continuations (
319
332
  agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
320
333
  objective TEXT NOT NULL,
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import { getCompactionWatch } from "./compaction-watch";
11
11
  import { getConfig, setConfig } from "./config-store";
12
12
  import { startConnectorStatusPoller } from "./connectors";
13
13
  import { installNotificationSubscriptionRule } from "./notification-subscription-rules";
14
+ import { installQuotaAdvisoryHandler } from "./quota-advisory";
14
15
  import { startPlanLiveBindings } from "./services/plan-live-bindings";
15
16
  import { resolve, sep } from "path";
16
17
  import { gzipSync, brotliCompressSync, constants as zlibConstants } from "node:zlib";
@@ -76,6 +77,7 @@ function startServer(): void {
76
77
 
77
78
  assertSafeNetworkConfig(HOST);
78
79
  initDb(DB_PATH);
80
+ installQuotaAdvisoryHandler();
79
81
  installNotificationSubscriptionRule();
80
82
  startPlanLiveBindings();
81
83
  getLifecycleManager().start();
@@ -5,6 +5,7 @@ export type NotificationType =
5
5
  | "agent.exited"
6
6
  | "agent.spawn_failed"
7
7
  | "agent.context_threshold"
8
+ | "agent.quota_threshold"
8
9
  | "agent.resume_budget_warning"
9
10
  | "agent.resume_budget_exhausted"
10
11
  | "agent.transient_land_hold"
@@ -54,6 +55,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
54
55
  "agent.exited": null,
55
56
  "agent.spawn_failed": null,
56
57
  "agent.context_threshold": 15 * MINUTES,
58
+ "agent.quota_threshold": 15 * MINUTES,
57
59
  "agent.resume_budget_warning": null,
58
60
  "agent.resume_budget_exhausted": null,
59
61
  "agent.transient_land_hold": null,