agent-relay-server 0.74.0 → 0.76.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 (69) hide show
  1. package/docs/openapi.json +39 -1
  2. package/package.json +2 -2
  3. package/public/assets/MessageBubble-DzhG1x8z.js +3 -0
  4. package/public/assets/{MessageBubble-CspI1qqW.js.map → MessageBubble-DzhG1x8z.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-WjXyWbzZ.js → PlanGraphPanel-brNJanGs.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-WjXyWbzZ.js.map → PlanGraphPanel-brNJanGs.js.map} +1 -1
  7. package/public/assets/{activity-B9WoIWSe.js → activity-DIXJ-CVu.js} +2 -2
  8. package/public/assets/{activity-B9WoIWSe.js.map → activity-DIXJ-CVu.js.map} +1 -1
  9. package/public/assets/{agents-D7th_n2N.js → agents-BG9XZcVD.js} +2 -2
  10. package/public/assets/{agents-D7th_n2N.js.map → agents-BG9XZcVD.js.map} +1 -1
  11. package/public/assets/{automation-wOEiCheJ.js → automation-jVNnsYV7.js} +2 -2
  12. package/public/assets/{automation-wOEiCheJ.js.map → automation-jVNnsYV7.js.map} +1 -1
  13. package/public/assets/{chat-Bj6R0vno.js → chat-B7EMdwIg.js} +2 -2
  14. package/public/assets/{chat-Bj6R0vno.js.map → chat-B7EMdwIg.js.map} +1 -1
  15. package/public/assets/{formatted-body-impl-BXeoUw5r.js → formatted-body-impl-DBWLuF0p.js} +2 -2
  16. package/public/assets/{formatted-body-impl-BXeoUw5r.js.map → formatted-body-impl-DBWLuF0p.js.map} +1 -1
  17. package/public/assets/index-DB6XGZy9.js +23 -0
  18. package/public/assets/index-DB6XGZy9.js.map +1 -0
  19. package/public/assets/index-DepEqGST.css +2 -0
  20. package/public/assets/{insights-n-pd12FP.js → insights-CzGE5EyU.js} +2 -2
  21. package/public/assets/{insights-n-pd12FP.js.map → insights-CzGE5EyU.js.map} +1 -1
  22. package/public/assets/{maintenance-DZI5exUu.js → maintenance-CGWGOhDP.js} +2 -2
  23. package/public/assets/{maintenance-DZI5exUu.js.map → maintenance-CGWGOhDP.js.map} +1 -1
  24. package/public/assets/{managed-agents-Bh_vowgH.js → managed-agents-DyYn12ir.js} +2 -2
  25. package/public/assets/{managed-agents-Bh_vowgH.js.map → managed-agents-DyYn12ir.js.map} +1 -1
  26. package/public/assets/{markdown-preview-impl-B-6jdhi0.js → markdown-preview-impl-BA37v8OC.js} +2 -2
  27. package/public/assets/{markdown-preview-impl-B-6jdhi0.js.map → markdown-preview-impl-BA37v8OC.js.map} +1 -1
  28. package/public/assets/{memory-B74VVg5K.js → memory-CoIM-761.js} +2 -2
  29. package/public/assets/{memory-B74VVg5K.js.map → memory-CoIM-761.js.map} +1 -1
  30. package/public/assets/{messages-D5n46hEh.js → messages-D-TOb6_t.js} +2 -2
  31. package/public/assets/{messages-D5n46hEh.js.map → messages-D-TOb6_t.js.map} +1 -1
  32. package/public/assets/{orchestrators-DBryzqq2.js → orchestrators-XLGUAnKs.js} +2 -2
  33. package/public/assets/{orchestrators-DBryzqq2.js.map → orchestrators-XLGUAnKs.js.map} +1 -1
  34. package/public/assets/{overview-0xb4ATEH.js → overview-C4Netp8S.js} +2 -2
  35. package/public/assets/{overview-0xb4ATEH.js.map → overview-C4Netp8S.js.map} +1 -1
  36. package/public/assets/{pairs-CnKDSHJu.js → pairs-1X219PK6.js} +2 -2
  37. package/public/assets/{pairs-CnKDSHJu.js.map → pairs-1X219PK6.js.map} +1 -1
  38. package/public/assets/{security-C2t99jNP.js → security-CCfKm-ow.js} +2 -2
  39. package/public/assets/{security-C2t99jNP.js.map → security-CCfKm-ow.js.map} +1 -1
  40. package/public/assets/{select-CCbQiEmS.js → select-BpPDknSO.js} +2 -2
  41. package/public/assets/{select-CCbQiEmS.js.map → select-BpPDknSO.js.map} +1 -1
  42. package/public/assets/{settings-Ucd8p6KS.js → settings-BUD05ZM3.js} +2 -2
  43. package/public/assets/{settings-Ucd8p6KS.js.map → settings-BUD05ZM3.js.map} +1 -1
  44. package/public/assets/{tasks-Bk8clApn.js → tasks-Dvqe1bbe.js} +2 -2
  45. package/public/assets/{tasks-Bk8clApn.js.map → tasks-Dvqe1bbe.js.map} +1 -1
  46. package/public/assets/{teams-BXkI8tvS.js → teams-I0UCuF9H.js} +2 -2
  47. package/public/assets/{teams-BXkI8tvS.js.map → teams-I0UCuF9H.js.map} +1 -1
  48. package/public/assets/{terminal-viewer-impl-BL-zWN1C.js → terminal-viewer-impl-CKM8zVGQ.js} +2 -2
  49. package/public/assets/{terminal-viewer-impl-BL-zWN1C.js.map → terminal-viewer-impl-CKM8zVGQ.js.map} +1 -1
  50. package/public/assets/{work-queue-DqMKcH5G.js → work-queue-xWRsNNPk.js} +2 -2
  51. package/public/assets/{work-queue-DqMKcH5G.js.map → work-queue-xWRsNNPk.js.map} +1 -1
  52. package/public/index.html +2 -2
  53. package/src/bus.ts +13 -5
  54. package/src/db/agents.ts +62 -7
  55. package/src/db/mappers.ts +18 -0
  56. package/src/db/migrations.ts +16 -0
  57. package/src/db/schema.ts +13 -0
  58. package/src/lifecycle-context-pressure.ts +98 -0
  59. package/src/lifecycle-manager.ts +21 -31
  60. package/src/quota.ts +40 -0
  61. package/src/routes/agents-spawn.ts +3 -0
  62. package/src/routes/agents.ts +14 -1
  63. package/src/routes/index.ts +2 -1
  64. package/src/upgrade-local-id.ts +23 -0
  65. package/src/upgrade.ts +19 -27
  66. package/public/assets/MessageBubble-CspI1qqW.js +0 -3
  67. package/public/assets/index-CQQTCZxg.css +0 -2
  68. package/public/assets/index-DNQadbv0.js +0 -22
  69. package/public/assets/index-DNQadbv0.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{Bn as e,an as t,ct as n,gn as r,i}from"./lucide-react-DI8kTavp.js";import{l as a,t as o}from"./store-D1K4_xId.js";import{U as s,d as c,ut as l}from"./display-DHgdn4vN.js";import{t as u}from"./badge-YQhT30dy.js";import{t as d}from"./button-CaFI76Ja.js";import{B as f,K as p,s as m}from"./index-DNQadbv0.js";import{r as h,t as g}from"./card-B_fQtKce.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),r=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(!r.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)(n,{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:r.from,onChange:e=>i({compose:{...r,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:n,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)(t,{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)(r,{className:`w-3 h-3 inline mr-1`}),s(n,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)(t,{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)(i,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-DqMKcH5G.js.map
1
+ import{Bn as e,an as t,ct as n,gn as r,i}from"./lucide-react-DI8kTavp.js";import{l as a,t as o}from"./store-D1K4_xId.js";import{U as s,d as c,ut as l}from"./display-DHgdn4vN.js";import{t as u}from"./badge-YQhT30dy.js";import{t as d}from"./button-CaFI76Ja.js";import{B as f,K as p,s as m}from"./index-DB6XGZy9.js";import{r as h,t as g}from"./card-B_fQtKce.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),r=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(!r.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)(n,{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:r.from,onChange:e=>i({compose:{...r,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:n,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)(t,{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)(r,{className:`w-3 h-3 inline mr-1`}),s(n,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)(t,{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)(i,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-xWRsNNPk.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-DqMKcH5G.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-xWRsNNPk.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-DNQadbv0.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-DB6XGZy9.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-DI8kTavp.js">
@@ -37,7 +37,7 @@
37
37
  <link rel="modulepreload" crossorigin href="./assets/badge-YQhT30dy.js">
38
38
  <link rel="modulepreload" crossorigin href="./assets/input-Cqqq0OFl.js">
39
39
  <link rel="modulepreload" crossorigin href="./assets/themes-mW9zZP4O.js">
40
- <link rel="stylesheet" crossorigin href="./assets/index-CQQTCZxg.css">
40
+ <link rel="stylesheet" crossorigin href="./assets/index-DepEqGST.css">
41
41
  </head>
42
42
  <body class="bg-background text-foreground antialiased">
43
43
  <div id="root"></div>
package/src/bus.ts CHANGED
@@ -17,6 +17,7 @@ import { enqueueContinuationInjectionAfterSelfResume } from "./self-resume";
17
17
  import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
18
18
  import { probeWorkspaceOnTurnEnd } from "./workspace-probe";
19
19
  import { busStaleGraceMs } from "./config";
20
+ import { quotaStateFromUnknown } from "./quota";
20
21
  import {
21
22
  BusProtocolError,
22
23
  parseBusFrame,
@@ -27,7 +28,7 @@ import {
27
28
  import { errMessage, isRecord, stringValue } from "agent-relay-sdk";
28
29
  import { agentProviderState } from "./provider-state";
29
30
  import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
30
- import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, RegisterAgentInput, Task } from "./types";
31
+ import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
31
32
 
32
33
  interface BusSocketData {
33
34
  kind: "bus";
@@ -295,7 +296,7 @@ function handleRegister(ws: BusWebSocket, frame: RegisterFrame): void {
295
296
  }
296
297
  let epoch = 0;
297
298
  if (payload.agentId) {
298
- const label = stringMeta(payload.meta, "label"), lifecycle = stringMeta(payload.meta, "lifecycle"), context = contextFromMeta(payload.meta);
299
+ const label = stringMeta(payload.meta, "label"), lifecycle = stringMeta(payload.meta, "lifecycle"), context = contextFromMeta(payload.meta), quota = quotaFromMeta(payload.meta);
299
300
  // Thin transport: build the unified RegisterAgentInput from the wire frame and hand it to
300
301
  // the registerAgent service. ALL registration side effects — authoritative parent lineage
301
302
  // (resolved from the token constraints inside the service, never the wire body), the managed
@@ -315,6 +316,7 @@ function handleRegister(ws: BusWebSocket, frame: RegisterFrame): void {
315
316
  instanceId: payload.instanceId,
316
317
  ...(providerCapabilities ? { providerCapabilities } : {}),
317
318
  ...(context ? { context } : {}),
319
+ ...(quota ? { quota } : {}),
318
320
  ...(lifecycle === "transient" || lifecycle === "persistent" ? { lifecycle } : {}),
319
321
  meta: payload.meta,
320
322
  };
@@ -348,12 +350,13 @@ function handleRegister(ws: BusWebSocket, frame: RegisterFrame): void {
348
350
  });
349
351
  }
350
352
 
351
- function runtimeFromMeta(meta: Record<string, unknown> | undefined): { providerCapabilities?: ProviderCapabilities; context?: ContextState } | undefined {
353
+ function runtimeFromMeta(meta: Record<string, unknown> | undefined): { providerCapabilities?: ProviderCapabilities; context?: ContextState; quota?: QuotaState } | undefined {
352
354
  if (!meta) return undefined;
353
355
  const providerCapabilities = providerCapabilitiesFromMeta(meta, [], false);
354
356
  const context = contextFromMeta(meta);
355
- if (!providerCapabilities && !context) return undefined;
356
- return { ...(providerCapabilities ? { providerCapabilities } : {}), ...(context ? { context } : {}) };
357
+ const quota = quotaFromMeta(meta);
358
+ if (!providerCapabilities && !context && !quota) return undefined;
359
+ return { ...(providerCapabilities ? { providerCapabilities } : {}), ...(context ? { context } : {}), ...(quota ? { quota } : {}) };
357
360
  }
358
361
 
359
362
  function providerCapabilitiesFromMeta(
@@ -393,6 +396,7 @@ function providerCapabilitiesFromMeta(
393
396
  lastUpdatedAt: now,
394
397
  },
395
398
  ...(isRecord(explicit?.context) ? { context: explicit.context as ProviderCapabilities["context"] } : {}),
399
+ ...(isRecord(explicit?.quota) ? { quota: explicit.quota as ProviderCapabilities["quota"] } : {}),
396
400
  ...(isRecord(explicit?.terminal) ? { terminal: explicit.terminal as ProviderCapabilities["terminal"] } : {}),
397
401
  source: "runtime",
398
402
  confidence: "reported",
@@ -423,6 +427,10 @@ function contextFromMeta(meta: Record<string, unknown>): ContextState | undefine
423
427
  };
424
428
  }
425
429
 
430
+ function quotaFromMeta(meta: Record<string, unknown>): QuotaState | undefined {
431
+ return quotaStateFromUnknown(meta.quota);
432
+ }
433
+
426
434
  function hasRunnerLifecycleCapabilities(capabilities: string[], meta: Record<string, unknown>): boolean {
427
435
  const lifecycle = isRecord(meta.lifecycleCapabilities) ? meta.lifecycleCapabilities : {};
428
436
  return capabilities.includes("lifecycle.shutdown.hard") &&
package/src/db/agents.ts CHANGED
@@ -19,7 +19,7 @@ import { evaluatePoolBindings, upsertChannelForAgent } from "./channels.ts";
19
19
  import { seedContinuationObjectiveFromSpawnPrompt } from "./continuations.ts";
20
20
  import { messageTaskClosed } from "./claim-sql.ts";
21
21
  import { ValidationError, getDb } from "./connection.ts";
22
- import { rowToAgent, rowToContextSnapshot } from "./mappers.ts";
22
+ import { rowToAgent, rowToContextSnapshot, rowToQuotaSnapshot } from "./mappers.ts";
23
23
  import { closeOpenPairsForAgent } from "./pair-maintenance.ts";
24
24
  import { parseStringArray } from "./shared.ts";
25
25
  import { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
@@ -83,6 +83,8 @@ import type {
83
83
  ContextSnapshot,
84
84
  ContextState,
85
85
  ProviderCapabilities,
86
+ QuotaSnapshot,
87
+ QuotaState,
86
88
  TaskStatusInput,
87
89
  WorkspaceMetadata,
88
90
  WorkspaceRecord,
@@ -157,9 +159,10 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
157
159
  const lifecycleProvided = Object.prototype.hasOwnProperty.call(input, "lifecycle");
158
160
  const instanceProvided = Boolean(input.instanceId);
159
161
  const contextState = mergeRelayContextMarkers(input.id, input.context);
162
+ const quotaChanged = input.quota ? hasQuotaChanged(input.id, input.quota) : false;
160
163
  const stmt = getDb().query(`
161
- INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, lifecycle, spawned_by, team_id, last_seen, created_at)
162
- VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $lifecycle, $spawnedBy, $teamId, $now, $now)
164
+ INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, quota_state, meta, lifecycle, spawned_by, team_id, last_seen, created_at)
165
+ VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $quotaState, $meta, $lifecycle, $spawnedBy, $teamId, $now, $now)
163
166
  ON CONFLICT(id) DO UPDATE SET
164
167
  name = $name,
165
168
  kind = $kind,
@@ -177,6 +180,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
177
180
  END,
178
181
  provider_capabilities = coalesce($providerCapabilities, agents.provider_capabilities),
179
182
  context_state = coalesce($contextState, agents.context_state),
183
+ quota_state = coalesce($quotaState, agents.quota_state),
180
184
  meta = $meta,
181
185
  lifecycle = CASE WHEN $lifecycleProvided = 1 THEN $lifecycle ELSE agents.lifecycle END,
182
186
  spawned_by = coalesce($spawnedBy, agents.spawned_by),
@@ -202,6 +206,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
202
206
  $initialEpoch: instanceProvided ? 1 : 0,
203
207
  $providerCapabilities: input.providerCapabilities ? JSON.stringify(input.providerCapabilities) : null,
204
208
  $contextState: contextState ? JSON.stringify(contextState) : null,
209
+ $quotaState: input.quota ? JSON.stringify(input.quota) : null,
205
210
  $meta: JSON.stringify(input.meta ?? {}),
206
211
  $lifecycle: input.lifecycle ?? "persistent",
207
212
  $lifecycleProvided: lifecycleProvided ? 1 : 0,
@@ -210,6 +215,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
210
215
  $now: now,
211
216
  });
212
217
  if (contextState) recordContextSnapshot(input.id, contextState, now);
218
+ if (input.quota && quotaChanged) recordQuotaSnapshot(input.id, input.quota, now);
213
219
 
214
220
  const agent = getAgent(input.id)!;
215
221
  seedContinuationObjectiveFromSpawnPrompt(agent.id, stringValue(agent.meta?.spawnRequestId), DEFAULT_SELF_RESUME_CONFIG, now);
@@ -347,28 +353,32 @@ export function recordAgentExitDiagnostics(id: string, diagnostics: ManagedSessi
347
353
  export function heartbeat(
348
354
  id: string,
349
355
  guard?: AgentSessionGuard,
350
- runtime?: { providerCapabilities?: ProviderCapabilities; context?: ContextState },
356
+ runtime?: { providerCapabilities?: ProviderCapabilities; context?: ContextState; quota?: QuotaState },
351
357
  ): boolean {
352
358
  if (!validateAgentSession(id, guard).ok) return false;
353
359
  const now = Date.now();
354
- if (runtime?.providerCapabilities || runtime?.context) {
360
+ if (runtime?.providerCapabilities || runtime?.context || runtime?.quota) {
355
361
  const contextState = mergeRelayContextMarkers(id, runtime.context);
362
+ const quotaChanged = runtime.quota ? hasQuotaChanged(id, runtime.quota) : false;
356
363
  const result = getDb()
357
364
  .query(`
358
365
  UPDATE agents SET
359
366
  last_seen = ?,
360
367
  status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END,
361
368
  provider_capabilities = coalesce(?, provider_capabilities),
362
- context_state = coalesce(?, context_state)
369
+ context_state = coalesce(?, context_state),
370
+ quota_state = coalesce(?, quota_state)
363
371
  WHERE id = ?
364
372
  `)
365
373
  .run(
366
374
  now,
367
375
  runtime.providerCapabilities ? JSON.stringify(runtime.providerCapabilities) : null,
368
376
  contextState ? JSON.stringify(contextState) : null,
377
+ runtime.quota ? JSON.stringify(runtime.quota) : null,
369
378
  id,
370
379
  );
371
380
  if (result.changes > 0 && contextState) recordContextSnapshot(id, contextState, now);
381
+ if (result.changes > 0 && runtime.quota && quotaChanged) recordQuotaSnapshot(id, runtime.quota, now);
372
382
  return result.changes > 0;
373
383
  }
374
384
  const result = getDb()
@@ -385,11 +395,56 @@ export function listContextSnapshots(agentId: string, options: { since?: number;
385
395
  sql += " AND captured_at >= ?";
386
396
  params.push(options.since);
387
397
  }
388
- sql += " ORDER BY captured_at ASC LIMIT ?";
398
+ sql += " ORDER BY captured_at ASC, id ASC LIMIT ?";
389
399
  params.push(limit);
390
400
  return (getDb().query(sql).all(...params) as any[]).map(rowToContextSnapshot);
391
401
  }
392
402
 
403
+ export function listQuotaSnapshots(agentId: string, options: { since?: number; limit?: number } = {}): QuotaSnapshot[] {
404
+ const limit = Math.max(1, Math.min(options.limit ?? 288, 1000));
405
+ const params: any[] = [agentId];
406
+ let sql = "SELECT * FROM quota_snapshots WHERE agent_id = ?";
407
+ if (options.since !== undefined) {
408
+ sql += " AND captured_at >= ?";
409
+ params.push(options.since);
410
+ }
411
+ sql += " ORDER BY captured_at ASC, id ASC LIMIT ?";
412
+ params.push(limit);
413
+ return (getDb().query(sql).all(...params) as any[]).map(rowToQuotaSnapshot);
414
+ }
415
+
416
+ function hasQuotaChanged(agentId: string, quota: QuotaState): boolean {
417
+ const row = getDb().query("SELECT quota_state FROM agents WHERE id = ?").get(agentId) as { quota_state?: string | null } | undefined;
418
+ return row?.quota_state !== JSON.stringify(quota);
419
+ }
420
+
421
+ function pruneQuotaSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
422
+ const result = agentId
423
+ ? getDb().query("DELETE FROM quota_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
424
+ : getDb().query("DELETE FROM quota_snapshots WHERE captured_at < ?").run(olderThan);
425
+ return result.changes;
426
+ }
427
+
428
+ function recordQuotaSnapshot(agentId: string, quota: QuotaState, now: number): void {
429
+ getDb().query(`
430
+ INSERT INTO quota_snapshots (
431
+ agent_id,
432
+ windows,
433
+ source,
434
+ quota_state,
435
+ captured_at
436
+ )
437
+ VALUES (?, ?, ?, ?, ?)
438
+ `).run(
439
+ agentId,
440
+ JSON.stringify(quota.windows),
441
+ quota.source,
442
+ JSON.stringify(quota),
443
+ now,
444
+ );
445
+ pruneQuotaSnapshots(agentId, now - DAY_MS);
446
+ }
447
+
393
448
  function pruneContextSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
394
449
  const result = agentId
395
450
  ? getDb().query("DELETE FROM context_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
package/src/db/mappers.ts CHANGED
@@ -72,6 +72,8 @@ import type {
72
72
  ContextSnapshot,
73
73
  ContextState,
74
74
  ProviderCapabilities,
75
+ QuotaSnapshot,
76
+ QuotaState,
75
77
  TaskStatusInput,
76
78
  WorkspaceMetadata,
77
79
  WorkspaceRecord,
@@ -127,6 +129,7 @@ export function rowToAgent(row: any): AgentCard {
127
129
  epoch: typeof row.epoch === "number" ? row.epoch : 0,
128
130
  providerCapabilities: parseJson<ProviderCapabilities | undefined>(row.provider_capabilities, undefined),
129
131
  context: parseJson<ContextState | undefined>(row.context_state, undefined),
132
+ quota: parseJson<QuotaState | undefined>(row.quota_state, undefined),
130
133
  meta: parseJson(row.meta, {}),
131
134
  lifecycle: row.lifecycle === "transient" ? "transient" : "persistent",
132
135
  spawnedBy: row.spawned_by ?? undefined,
@@ -161,6 +164,21 @@ export function rowToContextSnapshot(row: any): ContextSnapshot {
161
164
  };
162
165
  }
163
166
 
167
+ export function rowToQuotaSnapshot(row: any): QuotaSnapshot {
168
+ const quota = parseJson<QuotaState>(row.quota_state, {
169
+ windows: parseJson<QuotaState["windows"]>(row.windows, []),
170
+ source: row.source,
171
+ updatedAt: row.captured_at,
172
+ });
173
+ return {
174
+ agentId: row.agent_id,
175
+ capturedAt: row.captured_at,
176
+ windows: quota.windows,
177
+ source: quota.source,
178
+ updatedAt: quota.updatedAt,
179
+ };
180
+ }
181
+
164
182
  export function rowToMessage(row: any): Message {
165
183
  return {
166
184
  id: row.id,
@@ -76,6 +76,8 @@ import type {
76
76
  ContextSnapshot,
77
77
  ContextState,
78
78
  ProviderCapabilities,
79
+ QuotaSnapshot,
80
+ QuotaState,
79
81
  TaskStatusInput,
80
82
  WorkspaceMetadata,
81
83
  WorkspaceRecord,
@@ -537,6 +539,9 @@ export function applyMigrations(): void {
537
539
  if (!agentColNames.includes("context_state")) {
538
540
  getDb().run("ALTER TABLE agents ADD COLUMN context_state TEXT");
539
541
  }
542
+ if (!agentColNames.includes("quota_state")) {
543
+ getDb().run("ALTER TABLE agents ADD COLUMN quota_state TEXT");
544
+ }
540
545
  if (!agentColNames.includes("lifecycle")) {
541
546
  getDb().run("ALTER TABLE agents ADD COLUMN lifecycle TEXT NOT NULL DEFAULT 'persistent'");
542
547
  }
@@ -594,6 +599,17 @@ export function applyMigrations(): void {
594
599
  )
595
600
  `);
596
601
  getDb().run("CREATE INDEX IF NOT EXISTS idx_context_snapshots_agent_time ON context_snapshots(agent_id, captured_at DESC)");
602
+ getDb().run(`
603
+ CREATE TABLE IF NOT EXISTS quota_snapshots (
604
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
605
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
606
+ windows TEXT NOT NULL,
607
+ source TEXT NOT NULL,
608
+ quota_state TEXT NOT NULL,
609
+ captured_at INTEGER NOT NULL
610
+ )
611
+ `);
612
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC)");
597
613
 
598
614
  getDb().run(`
599
615
  CREATE TABLE IF NOT EXISTS memories (
package/src/db/schema.ts CHANGED
@@ -74,6 +74,8 @@ import type {
74
74
  ContextSnapshot,
75
75
  ContextState,
76
76
  ProviderCapabilities,
77
+ QuotaSnapshot,
78
+ QuotaState,
77
79
  TaskStatusInput,
78
80
  WorkspaceMetadata,
79
81
  WorkspaceRecord,
@@ -104,6 +106,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
104
106
  epoch INTEGER NOT NULL DEFAULT 0,
105
107
  provider_capabilities TEXT,
106
108
  context_state TEXT,
109
+ quota_state TEXT,
107
110
  meta TEXT NOT NULL DEFAULT '{}', lifecycle TEXT NOT NULL DEFAULT 'persistent',
108
111
  spawned_by TEXT,
109
112
  team_id TEXT,
@@ -287,6 +290,16 @@ export function initDb(path: string = "agent-relay.db"): Database {
287
290
  );
288
291
  CREATE INDEX IF NOT EXISTS idx_context_snapshots_agent_time ON context_snapshots(agent_id, captured_at DESC);
289
292
 
293
+ CREATE TABLE IF NOT EXISTS quota_snapshots (
294
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
295
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
296
+ windows TEXT NOT NULL,
297
+ source TEXT NOT NULL,
298
+ quota_state TEXT NOT NULL,
299
+ captured_at INTEGER NOT NULL
300
+ );
301
+ CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC);
302
+
290
303
  CREATE TABLE IF NOT EXISTS agent_continuations (
291
304
  agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
292
305
  objective TEXT NOT NULL,
@@ -0,0 +1,98 @@
1
+ import { createCommand } from "./commands-db";
2
+ import { getAgent, listAgents } from "./db";
3
+ import { getManagedAgentState } from "./config-store";
4
+ import { emitCommandEvent } from "./command-events";
5
+ import { handleContextPressure } from "./context-advisory";
6
+ import type { AgentCard, Command, SpawnPolicy } from "./types";
7
+
8
+ interface LifecycleContextPressureInput {
9
+ policies: SpawnPolicy[];
10
+ now: () => number;
11
+ hasActiveCommand: (target: string, type: string) => boolean;
12
+ }
13
+
14
+ interface CompactAgentInput {
15
+ agent: AgentCard;
16
+ reason: string;
17
+ policyName?: string;
18
+ alwaysReload?: string[];
19
+ now: () => number;
20
+ hasActiveCommand: (target: string, type: string) => boolean;
21
+ }
22
+
23
+ export function reconcileLifecycleContextPressure(input: LifecycleContextPressureInput): Set<string> {
24
+ const byPolicy = new Map(input.policies.map((policy) => [policy.name, policy]));
25
+ const candidates = new Map<string, { agent: AgentCard; policy?: SpawnPolicy }>();
26
+
27
+ for (const policy of input.policies) {
28
+ const state = getManagedAgentState(policy.name);
29
+ if (state?.status !== "running" || !state.agentId) continue;
30
+ const agent = getAgent(state.agentId);
31
+ if (!agent || agent.status === "offline") continue;
32
+ candidates.set(agent.id, { agent, policy });
33
+ }
34
+
35
+ for (const agent of listAgents()) {
36
+ if (!isLiveRunnerManagedAgent(agent) || candidates.has(agent.id)) continue;
37
+ const policyName = typeof agent.meta?.policyName === "string" ? agent.meta.policyName : undefined;
38
+ candidates.set(agent.id, { agent, policy: policyName ? byPolicy.get(policyName) : undefined });
39
+ }
40
+
41
+ const handled = new Set<string>();
42
+ for (const { agent, policy } of candidates.values()) {
43
+ if (handleContextPressure({
44
+ agent,
45
+ now: input.now,
46
+ hasActiveCommand: input.hasActiveCommand,
47
+ compact: (reason) => requestAgentCompact({
48
+ agent,
49
+ reason,
50
+ policyName: policy?.name,
51
+ alwaysReload: alwaysReloadTags(agent.tags),
52
+ now: input.now,
53
+ hasActiveCommand: input.hasActiveCommand,
54
+ }),
55
+ })) {
56
+ handled.add(agent.id);
57
+ }
58
+ }
59
+ return handled;
60
+ }
61
+
62
+ export function requestAgentCompact(input: CompactAgentInput): Command | null {
63
+ if (input.hasActiveCommand(input.agent.id, "agent.compact")) return null;
64
+ const spawnRequestId = typeof input.agent.meta?.spawnRequestId === "string" ? input.agent.meta.spawnRequestId : undefined;
65
+ const sessionName = typeof input.agent.meta?.sessionName === "string" ? input.agent.meta.sessionName : undefined;
66
+ const tmuxSession = typeof input.agent.meta?.tmuxSession === "string" ? input.agent.meta.tmuxSession : undefined;
67
+ const command = createCommand({
68
+ type: "agent.compact",
69
+ source: "system",
70
+ target: input.agent.id,
71
+ correlationId: spawnRequestId,
72
+ params: {
73
+ action: "compact",
74
+ agentId: input.agent.id,
75
+ ...(input.policyName ? { policyName: input.policyName } : {}),
76
+ ...(spawnRequestId ? { spawnRequestId } : {}),
77
+ ...(sessionName ? { sessionName } : {}),
78
+ ...(tmuxSession ? { tmuxSession } : {}),
79
+ reason: input.reason,
80
+ memory: input.alwaysReload?.length ? { alwaysReload: input.alwaysReload } : undefined,
81
+ requestedBy: "lifecycle-manager",
82
+ requestedAt: input.now(),
83
+ },
84
+ });
85
+ emitCommandEvent(command, "command.requested");
86
+ return command;
87
+ }
88
+
89
+ export function alwaysReloadTags(tags: string[]): string[] {
90
+ return tags
91
+ .filter((tag) => tag.startsWith("memory-reload:"))
92
+ .map((tag) => tag.slice("memory-reload:".length).trim())
93
+ .filter(Boolean);
94
+ }
95
+
96
+ function isLiveRunnerManagedAgent(agent: AgentCard): boolean {
97
+ return agent.meta?.runnerManaged === true && agent.status !== "offline" && agent.status !== "stale";
98
+ }
@@ -9,7 +9,7 @@ import {
9
9
  upsertManagedAgentState,
10
10
  } from "./config-store";
11
11
  import { emitRelayEvent } from "./events";
12
- import { handleContextPressure } from "./context-advisory";
12
+ import { alwaysReloadTags, reconcileLifecycleContextPressure, requestAgentCompact } from "./lifecycle-context-pressure";
13
13
  import { markManagedAgentRunning, emitManagedState } from "./services/managed-running";
14
14
  import { authContextFromSystem } from "./services/auth-context";
15
15
  import { emitCommandEvent } from "./command-events";
@@ -66,8 +66,10 @@ export class LifecycleManager {
66
66
  }
67
67
 
68
68
  async tick(): Promise<void> {
69
- for (const policy of this.loadPolicies()) {
70
- this.reconcilePolicy(policy);
69
+ const policies = this.loadPolicies();
70
+ const contextHandled = this.reconcileContextPressure(policies);
71
+ for (const policy of policies) {
72
+ this.reconcilePolicy(policy, contextHandled);
71
73
  }
72
74
  }
73
75
 
@@ -175,7 +177,7 @@ export class LifecycleManager {
175
177
  onConfigChanged(namespace: string, key: string): void {
176
178
  if (namespace !== "spawn-policy") return;
177
179
  const policy = this.loadPolicies().find((item) => item.name === key);
178
- if (policy) this.reconcilePolicy(policy);
180
+ if (policy) this.reconcilePolicy(policy, this.reconcileContextPressure([policy]));
179
181
  }
180
182
 
181
183
  onRelayUpdated(_newVersion: string): void {
@@ -309,7 +311,7 @@ export class LifecycleManager {
309
311
  return command;
310
312
  }
311
313
 
312
- private reconcilePolicy(policy: SpawnPolicy): void {
314
+ private reconcilePolicy(policy: SpawnPolicy, contextHandled = new Set<string>()): void {
313
315
  const state = getManagedAgentState(policy.name);
314
316
  const enabled = policy.enabled !== false;
315
317
  if (!state) {
@@ -354,9 +356,16 @@ export class LifecycleManager {
354
356
  this.stopAgent(policy, true, "idle-timeout");
355
357
  return;
356
358
  }
357
- if (handleContextPressure({ agent, now: this.now, hasActiveCommand: (target, type) => this.hasActiveCommand(target, type), compact: (reason) => this.compactAgent(policy, agent.id, reason, alwaysReloadTags(agent.tags)) })) return;
359
+ if (contextHandled.has(agent.id)) return;
358
360
  if (this.shouldCompact(agent)) {
359
- this.compactAgent(policy, agent.id, "context-threshold", alwaysReloadTags(agent.tags));
361
+ requestAgentCompact({
362
+ agent,
363
+ reason: "context-threshold",
364
+ policyName: policy.name,
365
+ alwaysReload: alwaysReloadTags(agent.tags),
366
+ now: this.now,
367
+ hasActiveCommand: (target, type) => this.hasActiveCommand(target, type),
368
+ });
360
369
  return;
361
370
  }
362
371
  if (policy.scheduledDailyRestart && this.dailyRestartDue(policy, state) && this.isIdle(policy, state, 3600)) {
@@ -384,24 +393,12 @@ export class LifecycleManager {
384
393
  }
385
394
  }
386
395
 
387
- private compactAgent(policy: SpawnPolicy, agentId: string, reason: string, alwaysReload: string[] = []): Command | null {
388
- if (this.hasActiveCommand(agentId, "agent.compact")) return null;
389
- const command = createCommand({
390
- type: "agent.compact",
391
- source: "system",
392
- target: agentId,
393
- params: {
394
- action: "compact",
395
- agentId,
396
- policyName: policy.name,
397
- reason,
398
- memory: alwaysReload.length ? { alwaysReload } : undefined,
399
- requestedBy: "lifecycle-manager",
400
- requestedAt: this.now(),
401
- },
396
+ private reconcileContextPressure(policies: SpawnPolicy[]): Set<string> {
397
+ return reconcileLifecycleContextPressure({
398
+ policies,
399
+ now: this.now,
400
+ hasActiveCommand: (target, type) => this.hasActiveCommand(target, type),
402
401
  });
403
- emitCommandEvent(command, "command.requested");
404
- return command;
405
402
  }
406
403
 
407
404
  private ensureStoppedState(policy: SpawnPolicy): void {
@@ -654,10 +651,3 @@ function agentTerminalFailureMessage(agent: ReturnType<typeof getAgent> | null):
654
651
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
655
652
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
656
653
  }
657
-
658
- function alwaysReloadTags(tags: string[]): string[] {
659
- return tags
660
- .filter((tag) => tag.startsWith("memory-reload:"))
661
- .map((tag) => tag.slice("memory-reload:".length).trim())
662
- .filter(Boolean);
663
- }