agent-relay-server 0.42.0 → 0.43.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 (76) hide show
  1. package/docs/openapi.json +85 -1
  2. package/package.json +2 -2
  3. package/public/assets/{activity-G6l1nOLj.js → activity--LYUyJtT.js} +2 -2
  4. package/public/assets/{activity-G6l1nOLj.js.map → activity--LYUyJtT.js.map} +1 -1
  5. package/public/assets/{agents-DBuCRS5A.js → agents-6mhIWvF-.js} +2 -2
  6. package/public/assets/{agents-DBuCRS5A.js.map → agents-6mhIWvF-.js.map} +1 -1
  7. package/public/assets/{analytics-BhwMtd0_.js → analytics-W-oZ8bsY.js} +2 -2
  8. package/public/assets/{analytics-BhwMtd0_.js.map → analytics-W-oZ8bsY.js.map} +1 -1
  9. package/public/assets/{automation-B7jnsYhH.js → automation-DouRxEN7.js} +2 -2
  10. package/public/assets/{automation-B7jnsYhH.js.map → automation-DouRxEN7.js.map} +1 -1
  11. package/public/assets/chat-DVu6bXmE.js +2 -0
  12. package/public/assets/chat-DVu6bXmE.js.map +1 -0
  13. package/public/assets/display-ConJ9cJB.js.map +1 -1
  14. package/public/assets/{formatted-body-impl-BXuR2gfL.js → formatted-body-impl-CthzMcxz.js} +2 -2
  15. package/public/assets/{formatted-body-impl-BXuR2gfL.js.map → formatted-body-impl-CthzMcxz.js.map} +1 -1
  16. package/public/assets/{index-CAnJGSBl.js → index-B1s-hGRR.js} +5 -5
  17. package/public/assets/{index-CAnJGSBl.js.map → index-B1s-hGRR.js.map} +1 -1
  18. package/public/assets/index-Ryvn1efN.css +2 -0
  19. package/public/assets/{maintenance-B1bX9ety.js → maintenance-CdDtYHE9.js} +2 -2
  20. package/public/assets/{maintenance-B1bX9ety.js.map → maintenance-CdDtYHE9.js.map} +1 -1
  21. package/public/assets/{managed-agents-D12-nGKd.js → managed-agents-DXigb0pj.js} +2 -2
  22. package/public/assets/{managed-agents-D12-nGKd.js.map → managed-agents-DXigb0pj.js.map} +1 -1
  23. package/public/assets/{markdown-preview-impl-BWd3swt8.js → markdown-preview-impl-BVES_oc1.js} +2 -2
  24. package/public/assets/{markdown-preview-impl-BWd3swt8.js.map → markdown-preview-impl-BVES_oc1.js.map} +1 -1
  25. package/public/assets/{memory-4ERBFJBZ.js → memory-DWAF794H.js} +2 -2
  26. package/public/assets/{memory-4ERBFJBZ.js.map → memory-DWAF794H.js.map} +1 -1
  27. package/public/assets/{messages-B3ZES72c.js → messages-D63r1IRE.js} +2 -2
  28. package/public/assets/{messages-B3ZES72c.js.map → messages-D63r1IRE.js.map} +1 -1
  29. package/public/assets/{orchestrators-qKPntM-a.js → orchestrators-rbEOs7pu.js} +2 -2
  30. package/public/assets/{orchestrators-qKPntM-a.js.map → orchestrators-rbEOs7pu.js.map} +1 -1
  31. package/public/assets/{overview-ekmeyPaO.js → overview-CeUZzyTa.js} +2 -2
  32. package/public/assets/{overview-ekmeyPaO.js.map → overview-CeUZzyTa.js.map} +1 -1
  33. package/public/assets/{pairs-B2QdTF0M.js → pairs-DJ_e1R5S.js} +2 -2
  34. package/public/assets/{pairs-B2QdTF0M.js.map → pairs-DJ_e1R5S.js.map} +1 -1
  35. package/public/assets/{security-WkMkEhb7.js → security-DyeypCyX.js} +2 -2
  36. package/public/assets/{security-WkMkEhb7.js.map → security-DyeypCyX.js.map} +1 -1
  37. package/public/assets/{settings-C5aXX3od.js → settings-CVW329oA.js} +2 -2
  38. package/public/assets/{settings-C5aXX3od.js.map → settings-CVW329oA.js.map} +1 -1
  39. package/public/assets/{tasks-ppuVeXkO.js → tasks-CR8BxsPc.js} +2 -2
  40. package/public/assets/{tasks-ppuVeXkO.js.map → tasks-CR8BxsPc.js.map} +1 -1
  41. package/public/assets/{terminal-viewer-impl-DXycIJKe.js → terminal-viewer-impl-CISUjIl9.js} +2 -2
  42. package/public/assets/{terminal-viewer-impl-DXycIJKe.js.map → terminal-viewer-impl-CISUjIl9.js.map} +1 -1
  43. package/public/assets/{work-queue-C8UzeekT.js → work-queue-ipT9bYqc.js} +2 -2
  44. package/public/assets/{work-queue-C8UzeekT.js.map → work-queue-ipT9bYqc.js.map} +1 -1
  45. package/public/index.html +2 -2
  46. package/src/agent-ref.ts +7 -4
  47. package/src/channel-target.ts +1 -0
  48. package/src/db/agent-search.ts +6 -0
  49. package/src/db/agents.ts +11 -2
  50. package/src/db/channels.ts +2 -1
  51. package/src/db/delivery.ts +4 -1
  52. package/src/db/index.ts +1 -0
  53. package/src/db/mappers.ts +1 -1
  54. package/src/db/message-reads.ts +10 -1
  55. package/src/db/migrations.ts +34 -1
  56. package/src/db/plans.ts +17 -2
  57. package/src/db/schema.ts +23 -2
  58. package/src/db/teams.ts +151 -0
  59. package/src/mcp-plan-tools.ts +1 -0
  60. package/src/mcp-team-tools.ts +87 -0
  61. package/src/mcp.ts +6 -1
  62. package/src/routes/index.ts +4 -0
  63. package/src/routes/integrations.ts +2 -1
  64. package/src/routes/messages.ts +1 -1
  65. package/src/routes/teams.ts +60 -0
  66. package/src/runtime-tokens.ts +9 -3
  67. package/src/security.ts +7 -1
  68. package/src/services/plan-graph.ts +2 -0
  69. package/src/services/register-agent.ts +3 -1
  70. package/src/services/spawn-agent.ts +4 -1
  71. package/src/services/team.ts +69 -0
  72. package/src/token-db.ts +3 -3
  73. package/src/validation.ts +2 -0
  74. package/public/assets/chat-KaK5_x65.js +0 -2
  75. package/public/assets/chat-KaK5_x65.js.map +0 -1
  76. package/public/assets/index-DzRKGPXS.css +0 -2
@@ -1,2 +1,2 @@
1
- import{Qt as e,at as t,i as n,jn as r,ln as i}from"./lucide-react-ChOGD4vc.js";import{a,t as o}from"./store-CtKmqMfx.js";import{H as s,ct as c,d as l}from"./display-ConJ9cJB.js";import{t as u}from"./badge-CFVr4fG9.js";import{t as d}from"./button-BuTu0MHx.js";import{L as f,U as p,s as m}from"./index-CAnJGSBl.js";import{n as h,t as g}from"./card-C3ErQ372.js";var _=r(),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(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!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&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{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:l(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-C8UzeekT.js.map
1
+ import{Qt as e,at as t,i as n,jn as r,ln as i}from"./lucide-react-ChOGD4vc.js";import{a,t as o}from"./store-CtKmqMfx.js";import{H as s,ct as c,d as l}from"./display-ConJ9cJB.js";import{t as u}from"./badge-CFVr4fG9.js";import{t as d}from"./button-BuTu0MHx.js";import{L as f,U as p,s as m}from"./index-B1s-hGRR.js";import{n as h,t as g}from"./card-C3ErQ372.js";var _=r(),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(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!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&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{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:l(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-ipT9bYqc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-C8UzeekT.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-ipT9bYqc.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-CAnJGSBl.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-B1s-hGRR.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-CDM1EFcn.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-ChOGD4vc.js">
@@ -37,7 +37,7 @@
37
37
  <link rel="modulepreload" crossorigin href="./assets/badge-CFVr4fG9.js">
38
38
  <link rel="modulepreload" crossorigin href="./assets/input-CawU8wfc.js">
39
39
  <link rel="modulepreload" crossorigin href="./assets/themes-mW9zZP4O.js">
40
- <link rel="stylesheet" crossorigin href="./assets/index-DzRKGPXS.css">
40
+ <link rel="stylesheet" crossorigin href="./assets/index-Ryvn1efN.css">
41
41
  </head>
42
42
  <body class="bg-background text-foreground antialiased">
43
43
  <div id="root"></div>
package/src/agent-ref.ts CHANGED
@@ -32,13 +32,14 @@ export function isAgentOnline(agent: AgentCard, now: number = Date.now()): boole
32
32
  }
33
33
 
34
34
  interface Selector {
35
- field: "id" | "label" | "name" | "tag" | "capability" | "machine" | "rig";
35
+ field: "id" | "label" | "name" | "tag" | "capability" | "machine" | "rig" | "team";
36
36
  value: string;
37
37
  }
38
38
 
39
39
  const SELECTOR_PREFIXES: Array<[string, Selector["field"]]> = [
40
40
  ["id:", "id"],
41
41
  ["label:", "label"],
42
+ ["team:", "team"],
42
43
  ["name:", "name"],
43
44
  ["tag:", "tag"],
44
45
  ["capability:", "capability"],
@@ -63,6 +64,7 @@ function matchesSelector(agent: AgentCard, selector: Selector): boolean {
63
64
  case "capability": return agent.capabilities.includes(selector.value);
64
65
  case "machine": return agent.machine === selector.value;
65
66
  case "rig": return agent.rig === selector.value;
67
+ case "team": return agent.teamId === selector.value;
66
68
  }
67
69
  }
68
70
 
@@ -152,6 +154,7 @@ export function targetMatchesAgent(target: string, agentId: string, agent?: Agen
152
154
  if (target.startsWith("tag:") && agent.tags.includes(target.slice(4))) return true;
153
155
  if (target.startsWith("cap:") && agent.capabilities.includes(target.slice(4))) return true;
154
156
  if (target.startsWith("label:") && agent.label === target.slice(6)) return true;
157
+ if (target.startsWith("team:") && agent.teamId === target.slice(5)) return true;
155
158
  return false;
156
159
  }
157
160
 
@@ -184,7 +187,7 @@ type SendPlan =
184
187
  | { kind: "not_found"; message: string }
185
188
  | { kind: "ambiguous"; message: string; candidates: AgentCard[] };
186
189
 
187
- const FANOUT_PREFIXES = ["tag:", "cap:", "capability:", "label:"];
190
+ const FANOUT_PREFIXES = ["tag:", "cap:", "capability:", "label:", "team:"];
188
191
  const PASSTHROUGH_PREFIXES = ["policy:", "orchestrator:", "pool:"];
189
192
  const RESERVED_TARGETS = new Set(["user", "system"]);
190
193
 
@@ -199,7 +202,7 @@ function describeAgent(agent: AgentCard): string {
199
202
  function ambiguousMessage(ref: string, candidates: AgentCard[]): string {
200
203
  const list = candidates.slice(0, 8).map(describeAgent).join(", ");
201
204
  const more = candidates.length > 8 ? `, +${candidates.length - 8} more` : "";
202
- return `agent reference "${ref}" is ambiguous — matches ${candidates.length}: ${list}${more}. Use a full id, label, or a fan-out selector (tag:/cap:/label:).`;
205
+ return `agent reference "${ref}" is ambiguous — matches ${candidates.length}: ${list}${more}. Use a full id, label, or a fan-out selector (tag:/cap:/label:/team:).`;
203
206
  }
204
207
 
205
208
  function notFoundMessage(ref: string, agents: AgentCard[]): string {
@@ -227,7 +230,7 @@ export function planSend(to: string, agents: AgentCard[], opts: ResolveOptions =
227
230
  return { kind: "fanout", to: target, receipt: fanoutReceipt(recipients) };
228
231
  }
229
232
  if (FANOUT_PREFIXES.some((p) => target.startsWith(p))) {
230
- const recipients = matchAgents(target.replace(/^cap:/, "capability:"), agents)
233
+ const recipients = matchAgents(target.replace(/^cap:/, "capability:"), agents, opts)
231
234
  .filter((a) => isAgentOnline(a, now)).map((a) => a.id);
232
235
  return { kind: "fanout", to: target, receipt: fanoutReceipt(recipients) };
233
236
  }
@@ -15,6 +15,7 @@ import type { ChannelRouteTarget } from "./types";
15
15
  export function parseChannelRouteTarget(target: string): ChannelRouteTarget {
16
16
  if (target.startsWith("pool:")) return { type: "pool", id: target.slice("pool:".length) };
17
17
  if (target.startsWith("policy:")) return { type: "policy", id: target.slice("policy:".length) };
18
+ if (target.startsWith("team:")) return { type: "team", id: target.slice("team:".length) };
18
19
  if (target.startsWith("label:")) return { type: "label", id: target.slice("label:".length) };
19
20
  if (target.startsWith("tag:")) return { type: "tag", id: target.slice("tag:".length) };
20
21
  if (target.startsWith("cap:")) return { type: "capability", id: target.slice("cap:".length) };
@@ -95,6 +95,8 @@ export interface AgentSearchFilter {
95
95
  status?: string | string[];
96
96
  /** Exact parent agent id — backs the `spawnedBy:` / `spawnedBy:me` fleet filter (#221). */
97
97
  spawnedBy?: string;
98
+ /** Exact durable team id — backs roster lookups for first-class teams (#356). */
99
+ teamId?: string;
98
100
  }
99
101
 
100
102
  export interface AgentSearchSort {
@@ -149,6 +151,10 @@ export function searchAgents(filter: AgentSearchFilter = {}, sort: AgentSearchSo
149
151
  clauses.push("spawned_by = ?");
150
152
  params.push(filter.spawnedBy);
151
153
  }
154
+ if (filter.teamId) {
155
+ clauses.push("team_id = ?");
156
+ params.push(filter.teamId);
157
+ }
152
158
 
153
159
  const statuses = searchValues(filter.status);
154
160
  if (!statuses.includes("all")) {
package/src/db/agents.ts CHANGED
@@ -157,8 +157,8 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
157
157
  const instanceProvided = Boolean(input.instanceId);
158
158
  const contextState = mergeRelayContextMarkers(input.id, input.context);
159
159
  const stmt = getDb().query(`
160
- INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, spawned_by, last_seen, created_at)
161
- VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $spawnedBy, $now, $now)
160
+ INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, spawned_by, team_id, last_seen, created_at)
161
+ VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $spawnedBy, $teamId, $now, $now)
162
162
  ON CONFLICT(id) DO UPDATE SET
163
163
  name = $name,
164
164
  kind = $kind,
@@ -178,6 +178,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
178
178
  context_state = coalesce($contextState, agents.context_state),
179
179
  meta = $meta,
180
180
  spawned_by = coalesce($spawnedBy, agents.spawned_by),
181
+ team_id = coalesce($teamId, agents.team_id),
181
182
  last_seen = $now
182
183
  `);
183
184
 
@@ -201,6 +202,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
201
202
  $contextState: contextState ? JSON.stringify(contextState) : null,
202
203
  $meta: JSON.stringify(input.meta ?? {}),
203
204
  $spawnedBy: input.spawnedBy ?? null,
205
+ $teamId: input.teamId ?? null,
204
206
  $now: now,
205
207
  });
206
208
  if (contextState) recordContextSnapshot(input.id, contextState, now);
@@ -235,6 +237,13 @@ export function setLabel(id: string, label: string | null): boolean {
235
237
  );
236
238
  }
237
239
 
240
+ export function setAgentTeam(agentId: string, teamId: string): AgentCard | null {
241
+ const result = getDb()
242
+ .query("UPDATE agents SET team_id = ?, last_seen = ? WHERE id = ?")
243
+ .run(teamId, Date.now(), agentId);
244
+ return result.changes > 0 ? getAgent(agentId) : null;
245
+ }
246
+
238
247
  export function setTags(id: string, tags: string[]): AgentCard | null {
239
248
  const normalized = [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
240
249
  const result = getDb().query("UPDATE agents SET tags = ?, last_seen = ? WHERE id = ?").run(JSON.stringify(normalized), Date.now(), id);
@@ -108,6 +108,7 @@ export function rowToChannelBinding(row: any): ChannelBinding {
108
108
  export function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
109
109
  if (target.type === "agent") return target.id;
110
110
  if (target.type === "label") return `label:${target.id}`;
111
+ if (target.type === "team") return `team:${target.id}`;
111
112
  if (target.type === "tag") return `tag:${target.id}`;
112
113
  if (target.type === "capability") return `cap:${target.id}`;
113
114
  if (target.type === "broadcast") return "broadcast";
@@ -160,6 +161,7 @@ export function channelTargetMatches(target: ChannelRouteTarget, channelId?: str
160
161
  const channelCandidates = candidates.filter((agent) => agentCanServeChannel(agent, channelId));
161
162
  if (target.type === "pool") return poolSelectorMatches(target.id, channelCandidates);
162
163
  if (target.type === "label") return channelCandidates.filter((agent) => agent.label === target.id);
164
+ if (target.type === "team") return channelCandidates.filter((agent) => agent.teamId === target.id);
163
165
  if (target.type === "tag") return channelCandidates.filter((agent) => agent.tags.includes(target.id));
164
166
  if (target.type === "capability") return channelCandidates.filter((agent) => agent.capabilities.includes(target.id));
165
167
  if (target.type === "broadcast") return channelCandidates;
@@ -573,4 +575,3 @@ export function resolveChannelRoutes(channelId: string, conversationId?: string)
573
575
  const exact = rows.filter((row) => row.conversation_key === conversationId);
574
576
  return (exact.length ? exact : rows.filter((row) => row.conversation_key === "")).map(rowToChannelBinding);
575
577
  }
576
-
@@ -174,6 +174,10 @@ export function matchingDeliveryAgents(target: string): AgentCard[] {
174
174
  const label = target.slice(6);
175
175
  return candidates.filter((agent) => agent.label === label);
176
176
  }
177
+ if (target.startsWith("team:")) {
178
+ const teamId = target.slice(5);
179
+ return candidates.filter((agent) => agent.teamId === teamId);
180
+ }
177
181
  return [];
178
182
  }
179
183
 
@@ -392,4 +396,3 @@ export function expireQueuedMessages(now: number = Date.now()): Message[] {
392
396
  })();
393
397
  return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
394
398
  }
395
-
package/src/db/index.ts CHANGED
@@ -24,3 +24,4 @@ export * from "./merge-lease.ts";
24
24
  export * from "./continuations.ts";
25
25
  export * from "./continuation-archives.ts";
26
26
  export * from "./plans.ts";
27
+ export * from "./teams.ts";
package/src/db/mappers.ts CHANGED
@@ -130,6 +130,7 @@ export function rowToAgent(row: any): AgentCard {
130
130
  context: parseJson<ContextState | undefined>(row.context_state, undefined),
131
131
  meta: parseJson(row.meta, {}),
132
132
  spawnedBy: row.spawned_by ?? undefined,
133
+ teamId: row.team_id ?? undefined,
133
134
  lastSeen: row.last_seen,
134
135
  createdAt: row.created_at,
135
136
  };
@@ -394,4 +395,3 @@ export const MSG_SELECT = `SELECT m.*, (
394
395
  ) AS reactions_json FROM messages m`;
395
396
 
396
397
  // --- Agents ---
397
-
@@ -162,6 +162,7 @@ export function pollMessages(query: PollQuery): Message[] {
162
162
  const agentTags = agent?.tags ?? [];
163
163
  const agentCaps = agent?.capabilities ?? [];
164
164
  const agentLabel = agent?.label;
165
+ const agentTeamId = agent?.teamId;
165
166
  const agentLegacyChannelTargets = legacyChannelTargets(agent);
166
167
 
167
168
  const conditions: string[] = [];
@@ -186,11 +187,19 @@ export function pollMessages(query: PollQuery): Message[] {
186
187
  targetClauses.push("to_target = ?");
187
188
  params.push(`label:${agentLabel}`);
188
189
  }
190
+ if (agentTeamId) {
191
+ targetClauses.push("to_target = ?");
192
+ params.push(`team:${agentTeamId}`);
193
+ }
189
194
  for (const target of agentLegacyChannelTargets) {
190
195
  targetClauses.push("to_target = ?");
191
196
  params.push(target);
192
197
  }
193
198
  conditions.push(`(${targetClauses.join(" OR ")})`);
199
+ if (agentTeamId) {
200
+ conditions.push("(from_agent != ? OR to_target != ?)");
201
+ params.push(query.for, `team:${agentTeamId}`);
202
+ }
194
203
  conditions.push("delivery_status != 'queued' AND delivery_status NOT IN ('failed', 'dead')");
195
204
 
196
205
  // Hide active claims held by someone else, but let expired claims surface so
@@ -323,7 +332,7 @@ export function markRead(messageId: number, agentId: string): boolean {
323
332
  delivery_next_retry_at = NULL,
324
333
  delivery_poison_reason = NULL,
325
334
  delivery_updated_at = ?
326
- WHERE id = ? AND (to_target = ? OR resolved_to_agent = ? OR to_target = 'broadcast' OR to_target LIKE 'tag:%' OR to_target LIKE 'cap:%' OR to_target LIKE 'label:%')
335
+ WHERE id = ? AND (to_target = ? OR resolved_to_agent = ? OR to_target = 'broadcast' OR to_target LIKE 'tag:%' OR to_target LIKE 'cap:%' OR to_target LIKE 'label:%' OR to_target LIKE 'team:%')
327
336
  `).run(Date.now(), messageId, agentId, agentId);
328
337
  return true;
329
338
  }
@@ -279,15 +279,44 @@ export function applyMigrations(): void {
279
279
  // which runs per persisted session step. Added here (not the base schema) because the column
280
280
  // is migration-added for pre-hardening DBs. Without it that lookup full-scans messages.
281
281
  getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key)");
282
+ getDb().run(`
283
+ CREATE TABLE IF NOT EXISTS teams (
284
+ id TEXT PRIMARY KEY,
285
+ coordinator_id TEXT,
286
+ root_coordinator_id TEXT,
287
+ title TEXT,
288
+ brief TEXT,
289
+ definition_of_done TEXT,
290
+ status TEXT NOT NULL DEFAULT 'active',
291
+ outcome_status TEXT,
292
+ outcome_summary TEXT,
293
+ composition TEXT,
294
+ cost_tokens INTEGER,
295
+ cost_usd REAL,
296
+ wall_clock_ms INTEGER,
297
+ lands_clean INTEGER,
298
+ created_at INTEGER NOT NULL,
299
+ updated_at INTEGER NOT NULL,
300
+ dissolved_at INTEGER
301
+ )
302
+ `);
303
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id)");
304
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status)");
282
305
  getDb().run(`
283
306
  CREATE TABLE IF NOT EXISTS plans (
284
- id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
307
+ id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
285
308
  title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
286
309
  created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
287
310
  )
288
311
  `);
312
+ const planCols = getDb().query("PRAGMA table_info(plans)").all() as any[];
313
+ const planColNames = planCols.map((c: any) => c.name);
314
+ if (!planColNames.includes("team_id")) {
315
+ getDb().run("ALTER TABLE plans ADD COLUMN team_id TEXT");
316
+ }
289
317
  getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''))");
290
318
  getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_updated ON plans(updated_at)");
319
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_team ON plans(team_id)");
291
320
  getDb().run(`
292
321
  CREATE TABLE IF NOT EXISTS plan_versions (
293
322
  id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
@@ -372,6 +401,10 @@ export function applyMigrations(): void {
372
401
  if (!agentColNames.includes("spawned_by")) {
373
402
  getDb().run("ALTER TABLE agents ADD COLUMN spawned_by TEXT");
374
403
  }
404
+ if (!agentColNames.includes("team_id")) {
405
+ getDb().run("ALTER TABLE agents ADD COLUMN team_id TEXT");
406
+ }
407
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_team ON agents(team_id)");
375
408
  getDb().run(`
376
409
  CREATE TABLE IF NOT EXISTS context_snapshots (
377
410
  id INTEGER PRIMARY KEY AUTOINCREMENT,
package/src/db/plans.ts CHANGED
@@ -6,6 +6,7 @@ import type { Plan, PlanEdge, PlanNode } from "../types";
6
6
  interface PlanRow {
7
7
  id: string;
8
8
  thread_id: number;
9
+ team_id: string | null;
9
10
  channel: string | null;
10
11
  version: number;
11
12
  title: string | null;
@@ -49,6 +50,7 @@ function rowToPlan(row: PlanRow): Plan {
49
50
  return {
50
51
  id: row.id,
51
52
  threadId: String(row.thread_id),
53
+ ...(row.team_id ? { teamId: row.team_id } : {}),
52
54
  ...(row.channel ? { channel: row.channel } : {}),
53
55
  version: row.version,
54
56
  ...(row.title ? { title: row.title } : {}),
@@ -82,6 +84,7 @@ export function planThreadExists(threadId: number): boolean {
82
84
  export function createPlanRow(input: {
83
85
  id?: string;
84
86
  threadId: number;
87
+ teamId?: string;
85
88
  channel?: string;
86
89
  title?: string;
87
90
  objective?: string;
@@ -93,11 +96,12 @@ export function createPlanRow(input: {
93
96
  const now = Date.now();
94
97
  try {
95
98
  getDb().query(`
96
- INSERT INTO plans (id, thread_id, channel, version, title, objective, nodes, edges, created_by, created_at, updated_at)
97
- VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
99
+ INSERT INTO plans (id, thread_id, team_id, channel, version, title, objective, nodes, edges, created_by, created_at, updated_at)
100
+ VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
98
101
  `).run(
99
102
  id,
100
103
  input.threadId,
104
+ input.teamId ?? null,
101
105
  input.channel ?? null,
102
106
  input.title ?? null,
103
107
  input.objective ?? null,
@@ -128,6 +132,17 @@ export function getPlanByThread(threadId: number, channel?: string): Plan | null
128
132
  return row ? rowToPlan(row) : null;
129
133
  }
130
134
 
135
+ export function getPlanByTeam(teamId: string, channel?: string): Plan | null {
136
+ const row = channel === undefined
137
+ ? getDb()
138
+ .query("SELECT * FROM plans WHERE team_id = ? ORDER BY updated_at DESC LIMIT 1")
139
+ .get(teamId) as PlanRow | undefined
140
+ : getDb()
141
+ .query("SELECT * FROM plans WHERE team_id = ? AND coalesce(channel, '') = ? ORDER BY updated_at DESC LIMIT 1")
142
+ .get(teamId, channel) as PlanRow | undefined;
143
+ return row ? rowToPlan(row) : null;
144
+ }
145
+
131
146
  export function replacePlan(input: Plan, changedBy?: string): Plan {
132
147
  const existing = getPlan(input.id);
133
148
  if (!existing) throw new ValidationError(`plan ${input.id} not found`);
package/src/db/schema.ts CHANGED
@@ -106,10 +106,10 @@ export function initDb(path: string = "agent-relay.db"): Database {
106
106
  context_state TEXT,
107
107
  meta TEXT NOT NULL DEFAULT '{}',
108
108
  spawned_by TEXT,
109
+ team_id TEXT,
109
110
  last_seen INTEGER NOT NULL,
110
111
  created_at INTEGER NOT NULL
111
112
  );
112
-
113
113
  CREATE TABLE IF NOT EXISTS messages (
114
114
  id INTEGER PRIMARY KEY AUTOINCREMENT,
115
115
  from_agent TEXT NOT NULL,
@@ -150,8 +150,29 @@ export function initDb(path: string = "agent-relay.db"): Database {
150
150
  -- per candidate row (O(n^2)): ~8.6s at 4k rows, which blew the 5s Stop-hook
151
151
  -- timeout and wedged turns in "busy" (#199).
152
152
  CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent);
153
+ CREATE TABLE IF NOT EXISTS teams (
154
+ id TEXT PRIMARY KEY,
155
+ coordinator_id TEXT,
156
+ root_coordinator_id TEXT,
157
+ title TEXT,
158
+ brief TEXT,
159
+ definition_of_done TEXT,
160
+ status TEXT NOT NULL DEFAULT 'active',
161
+ outcome_status TEXT,
162
+ outcome_summary TEXT,
163
+ composition TEXT,
164
+ cost_tokens INTEGER,
165
+ cost_usd REAL,
166
+ wall_clock_ms INTEGER,
167
+ lands_clean INTEGER,
168
+ created_at INTEGER NOT NULL,
169
+ updated_at INTEGER NOT NULL,
170
+ dissolved_at INTEGER
171
+ );
172
+ CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id);
173
+ CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status);
153
174
  CREATE TABLE IF NOT EXISTS plans (
154
- id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
175
+ id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
155
176
  title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
156
177
  created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
157
178
  );
@@ -0,0 +1,151 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getAgent, setAgentTeam } from "./agents.ts";
3
+ import { getDb, ValidationError } from "./connection.ts";
4
+ import { rowToAgent } from "./mappers.ts";
5
+ import type { AgentCard, Team } from "../types";
6
+
7
+ interface TeamRow {
8
+ id: string;
9
+ coordinator_id: string | null;
10
+ root_coordinator_id: string | null;
11
+ title: string | null;
12
+ brief: string | null;
13
+ definition_of_done: string | null;
14
+ status: Team["status"];
15
+ outcome_status: Team["outcomeStatus"] | null;
16
+ outcome_summary: string | null;
17
+ composition: string | null;
18
+ cost_tokens: number | null;
19
+ cost_usd: number | null;
20
+ wall_clock_ms: number | null;
21
+ lands_clean: number | null;
22
+ created_at: number;
23
+ updated_at: number;
24
+ dissolved_at: number | null;
25
+ }
26
+
27
+ export interface TeamDissolveInput {
28
+ outcomeStatus?: Team["outcomeStatus"];
29
+ outcomeSummary?: string;
30
+ landsClean?: boolean;
31
+ }
32
+
33
+ export function teamId(): string {
34
+ return `team_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
35
+ }
36
+
37
+ function rowToTeam(row: TeamRow): Team {
38
+ return {
39
+ id: row.id,
40
+ ...(row.coordinator_id ? { coordinatorId: row.coordinator_id } : {}),
41
+ ...(row.root_coordinator_id ? { rootCoordinatorId: row.root_coordinator_id } : {}),
42
+ ...(row.title ? { title: row.title } : {}),
43
+ ...(row.brief ? { brief: row.brief } : {}),
44
+ ...(row.definition_of_done ? { definitionOfDone: row.definition_of_done } : {}),
45
+ status: row.status,
46
+ ...(row.outcome_status ? { outcomeStatus: row.outcome_status } : {}),
47
+ ...(row.outcome_summary ? { outcomeSummary: row.outcome_summary } : {}),
48
+ ...(row.composition ? { composition: row.composition } : {}),
49
+ ...(row.cost_tokens !== null ? { costTokens: row.cost_tokens } : {}),
50
+ ...(row.cost_usd !== null ? { costUsd: row.cost_usd } : {}),
51
+ ...(row.wall_clock_ms !== null ? { wallClockMs: row.wall_clock_ms } : {}),
52
+ ...(row.lands_clean !== null ? { landsClean: row.lands_clean === 1 } : {}),
53
+ createdAt: row.created_at,
54
+ updatedAt: row.updated_at,
55
+ ...(row.dissolved_at !== null ? { dissolvedAt: row.dissolved_at } : {}),
56
+ };
57
+ }
58
+
59
+ export function getTeam(id: string): Team | null {
60
+ const row = getDb().query("SELECT * FROM teams WHERE id = ?").get(id) as TeamRow | undefined;
61
+ return row ? rowToTeam(row) : null;
62
+ }
63
+
64
+ export function getTeamRoster(teamId: string): AgentCard[] {
65
+ const rows = getDb()
66
+ .query("SELECT * FROM agents WHERE team_id = ? ORDER BY created_at ASC, id ASC")
67
+ .all(teamId) as any[];
68
+ return rows.map(rowToAgent);
69
+ }
70
+
71
+ export function ensureTeam(coordinatorId: string): Team {
72
+ const coordinator = getAgent(coordinatorId);
73
+ if (!coordinator) throw new ValidationError(`coordinator agent ${coordinatorId} not found`);
74
+ if (coordinator.teamId) {
75
+ const existing = getTeam(coordinator.teamId);
76
+ if (existing) return existing;
77
+ }
78
+
79
+ const id = teamId();
80
+ const now = Date.now();
81
+ getDb().transaction(() => {
82
+ getDb().query(`
83
+ INSERT INTO teams (id, coordinator_id, root_coordinator_id, status, created_at, updated_at)
84
+ VALUES (?, ?, ?, 'active', ?, ?)
85
+ `).run(id, coordinatorId, coordinatorId, now, now);
86
+ setAgentTeam(coordinatorId, id);
87
+ })();
88
+ return getTeam(id)!;
89
+ }
90
+
91
+ export function setTeamBrief(input: {
92
+ teamId: string;
93
+ title?: string;
94
+ brief?: string;
95
+ definitionOfDone?: string;
96
+ }): Team {
97
+ const existing = getTeam(input.teamId);
98
+ if (!existing) throw new ValidationError(`team ${input.teamId} not found`);
99
+ const now = Date.now();
100
+ getDb().query(`
101
+ UPDATE teams
102
+ SET title = ?, brief = ?, definition_of_done = ?, updated_at = ?
103
+ WHERE id = ?
104
+ `).run(
105
+ input.title ?? null,
106
+ input.brief ?? null,
107
+ input.definitionOfDone ?? null,
108
+ now,
109
+ input.teamId,
110
+ );
111
+ return getTeam(input.teamId)!;
112
+ }
113
+
114
+ function compositionForMembers(members: AgentCard[]): string {
115
+ return JSON.stringify(members.map((agent) => ({
116
+ id: agent.id,
117
+ label: agent.label,
118
+ role: typeof agent.meta?.role === "string" ? agent.meta.role : undefined,
119
+ provider: agent.providerCapabilities?.model?.provider,
120
+ model: agent.providerCapabilities?.model?.id ?? agent.providerCapabilities?.model?.alias,
121
+ })));
122
+ }
123
+
124
+ export function dissolveTeam(teamId: string, input: TeamDissolveInput = {}): Team {
125
+ const existing = getTeam(teamId);
126
+ if (!existing) throw new ValidationError(`team ${teamId} not found`);
127
+ const members = getTeamRoster(teamId);
128
+ const now = Date.now();
129
+ getDb().query(`
130
+ UPDATE teams
131
+ SET status = 'dissolved',
132
+ outcome_status = ?,
133
+ outcome_summary = ?,
134
+ composition = ?,
135
+ wall_clock_ms = ?,
136
+ lands_clean = ?,
137
+ dissolved_at = ?,
138
+ updated_at = ?
139
+ WHERE id = ?
140
+ `).run(
141
+ input.outcomeStatus ?? null,
142
+ input.outcomeSummary ?? null,
143
+ compositionForMembers(members),
144
+ Math.max(0, now - existing.createdAt),
145
+ input.landsClean === undefined ? null : input.landsClean ? 1 : 0,
146
+ now,
147
+ now,
148
+ teamId,
149
+ );
150
+ return getTeam(teamId)!;
151
+ }
@@ -47,6 +47,7 @@ export const PLAN_TOOLS = [
47
47
  type: "object",
48
48
  properties: {
49
49
  threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] },
50
+ teamId: { type: "string" },
50
51
  channel: { type: "string" },
51
52
  title: { type: "string" },
52
53
  objective: { type: "string" },