agent-relay-server 0.79.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +55 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-C-3IcqgU.js → MessageBubble-DcSxvVGQ.js} +2 -2
- package/public/assets/{MessageBubble-C-3IcqgU.js.map → MessageBubble-DcSxvVGQ.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-o3u36Del.js → PlanGraphPanel-C_Qm7s4q.js} +2 -2
- package/public/assets/{PlanGraphPanel-o3u36Del.js.map → PlanGraphPanel-C_Qm7s4q.js.map} +1 -1
- package/public/assets/{activity-L3POfhGa.js → activity-HoO4RRm1.js} +2 -2
- package/public/assets/{activity-L3POfhGa.js.map → activity-HoO4RRm1.js.map} +1 -1
- package/public/assets/{agents-DAdDtQb6.js → agents-CSYMFVpW.js} +2 -2
- package/public/assets/{agents-DAdDtQb6.js.map → agents-CSYMFVpW.js.map} +1 -1
- package/public/assets/{automation-kakk3xZ9.js → automation-Bj2AYEw0.js} +2 -2
- package/public/assets/{automation-kakk3xZ9.js.map → automation-Bj2AYEw0.js.map} +1 -1
- package/public/assets/{chat-DDlRjwGC.js → chat-C410UoF1.js} +2 -2
- package/public/assets/{chat-DDlRjwGC.js.map → chat-C410UoF1.js.map} +1 -1
- package/public/assets/{formatted-body-impl-CEqSWNRL.js → formatted-body-impl-CzKd4psz.js} +2 -2
- package/public/assets/{formatted-body-impl-CEqSWNRL.js.map → formatted-body-impl-CzKd4psz.js.map} +1 -1
- package/public/assets/{index-CtGaz-Yg.js → index-DW4YIA7L.js} +5 -5
- package/public/assets/{index-CtGaz-Yg.js.map → index-DW4YIA7L.js.map} +1 -1
- package/public/assets/{insights-mf5sd483.js → insights-DgGTwAwZ.js} +2 -2
- package/public/assets/{insights-mf5sd483.js.map → insights-DgGTwAwZ.js.map} +1 -1
- package/public/assets/{maintenance-CruMfiA7.js → maintenance-CmgIgjo4.js} +2 -2
- package/public/assets/{maintenance-CruMfiA7.js.map → maintenance-CmgIgjo4.js.map} +1 -1
- package/public/assets/{managed-agents-fOhHGzNI.js → managed-agents-DRxAGFUB.js} +2 -2
- package/public/assets/{managed-agents-fOhHGzNI.js.map → managed-agents-DRxAGFUB.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-g5-PTM1A.js → markdown-preview-impl-DSquGj2d.js} +2 -2
- package/public/assets/{markdown-preview-impl-g5-PTM1A.js.map → markdown-preview-impl-DSquGj2d.js.map} +1 -1
- package/public/assets/{memory-CRyq2WNC.js → memory-BYq4eEF_.js} +2 -2
- package/public/assets/{memory-CRyq2WNC.js.map → memory-BYq4eEF_.js.map} +1 -1
- package/public/assets/{messages-BqLp0X3y.js → messages-BZt9N6sy.js} +2 -2
- package/public/assets/{messages-BqLp0X3y.js.map → messages-BZt9N6sy.js.map} +1 -1
- package/public/assets/{orchestrators-BD4nZcG3.js → orchestrators-DoqpuP2u.js} +2 -2
- package/public/assets/{orchestrators-BD4nZcG3.js.map → orchestrators-DoqpuP2u.js.map} +1 -1
- package/public/assets/{overview-De-2GN06.js → overview-CtpqcW4U.js} +2 -2
- package/public/assets/{overview-De-2GN06.js.map → overview-CtpqcW4U.js.map} +1 -1
- package/public/assets/{pairs-DOqjBd0R.js → pairs-XoWsTxhe.js} +2 -2
- package/public/assets/{pairs-DOqjBd0R.js.map → pairs-XoWsTxhe.js.map} +1 -1
- package/public/assets/{security-DtoRSpIi.js → security-B_C5yJi5.js} +2 -2
- package/public/assets/{security-DtoRSpIi.js.map → security-B_C5yJi5.js.map} +1 -1
- package/public/assets/{select-esP750zQ.js → select-BBZ8pgCn.js} +2 -2
- package/public/assets/{select-esP750zQ.js.map → select-BBZ8pgCn.js.map} +1 -1
- package/public/assets/{settings-U8788lYt.js → settings-DNYEfnw6.js} +2 -2
- package/public/assets/{settings-U8788lYt.js.map → settings-DNYEfnw6.js.map} +1 -1
- package/public/assets/{tasks-CtiwQy9I.js → tasks-Bww7kqYc.js} +2 -2
- package/public/assets/{tasks-CtiwQy9I.js.map → tasks-Bww7kqYc.js.map} +1 -1
- package/public/assets/teams-C6B038l0.js +2 -0
- package/public/assets/teams-C6B038l0.js.map +1 -0
- package/public/assets/{terminal-viewer-impl-h9hvvB9S.js → terminal-viewer-impl-CvoK2Byo.js} +2 -2
- package/public/assets/{terminal-viewer-impl-h9hvvB9S.js.map → terminal-viewer-impl-CvoK2Byo.js.map} +1 -1
- package/public/assets/{work-queue-C-bS6YmI.js → work-queue-w_eT-XFN.js} +2 -2
- package/public/assets/{work-queue-C-bS6YmI.js.map → work-queue-w_eT-XFN.js.map} +1 -1
- package/public/index.html +1 -1
- package/src/db/teams.ts +31 -4
- package/src/maintenance/jobs.ts +9 -1
- package/src/maintenance/teams.ts +116 -0
- package/src/notification-types.ts +2 -0
- package/src/routes/index.ts +3 -0
- package/src/routes/teams-config.ts +15 -0
- package/src/routes/teams.ts +11 -4
- package/src/security.ts +3 -1
- package/src/services/team.ts +17 -4
- package/src/services/transient-agent-reaper.ts +36 -0
- package/src/settings/registry.ts +27 -0
- package/src/team-idle-tracker.ts +13 -0
- package/src/teams-config-store.ts +22 -0
- package/public/assets/teams-Cx29VUif.js +0 -2
- package/public/assets/teams-Cx29VUif.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Hn as e,i as t,lt as n,sn as r,vn as i}from"./lucide-react-CNGB8fo1.js";import{l as a,t as o}from"./store-Cjcd0egy.js";import{U as s,d as c,ut as l}from"./display-DHgdn4vN.js";import{t as u}from"./badge-DGB2nVM0.js";import{t as d}from"./button-D_Y8CEP4.js";import{B as f,K as p,s as m}from"./index-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
1
|
+
import{Hn as e,i as t,lt as n,sn as r,vn as i}from"./lucide-react-CNGB8fo1.js";import{l as a,t as o}from"./store-Cjcd0egy.js";import{U as s,d as c,ut as l}from"./display-DHgdn4vN.js";import{t as u}from"./badge-DGB2nVM0.js";import{t as d}from"./button-D_Y8CEP4.js";import{B as f,K as p,s as m}from"./index-DW4YIA7L.js";import{r as h,t as g}from"./card-DTLNgk0R.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)(r,{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)(i,{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)(r,{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)(t,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
|
|
2
|
+
//# sourceMappingURL=work-queue-w_eT-XFN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-queue-C-bS6YmI.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-w_eT-XFN.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-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-DW4YIA7L.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-CNGB8fo1.js">
|
package/src/db/teams.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getAgent, setAgentTeam } from "./agents.ts";
|
|
|
3
3
|
import { getDb, ValidationError } from "./connection.ts";
|
|
4
4
|
import { rowToAgent } from "./mappers.ts";
|
|
5
5
|
import { unpromotedBlackboardEntries, type UnpromotedBlackboardSummary } from "./project-entries.ts";
|
|
6
|
+
import { isProviderBlocked } from "../provider-state.ts";
|
|
6
7
|
import type { AgentCard, Team } from "../types";
|
|
7
8
|
|
|
8
9
|
interface TeamRow {
|
|
@@ -72,6 +73,11 @@ export interface TeamOutcomeAggregates {
|
|
|
72
73
|
recent: TeamOutcomeRecent[];
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
interface TeamBusyState {
|
|
77
|
+
busyMemberIds: string[];
|
|
78
|
+
lastBusyAt?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
export function teamId(): string {
|
|
76
82
|
return `team_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
77
83
|
}
|
|
@@ -125,11 +131,32 @@ export function getTeamRoster(teamId: string): AgentCard[] {
|
|
|
125
131
|
return rows.map(rowToAgent);
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
export
|
|
134
|
+
export interface TeamMemberStats {
|
|
135
|
+
total: number;
|
|
136
|
+
live: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getTeamMemberStats(teamId: string, cutoff: number): TeamMemberStats {
|
|
129
140
|
const row = getDb().query(
|
|
130
|
-
"SELECT count(*) AS
|
|
131
|
-
).get(
|
|
132
|
-
return row.
|
|
141
|
+
"SELECT count(*) AS total, count(CASE WHEN status != 'offline' AND last_seen > ? THEN 1 END) AS live FROM agents WHERE team_id = ?",
|
|
142
|
+
).get(cutoff, teamId) as { total: number; live: number };
|
|
143
|
+
return { total: row.total, live: row.live };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function countLiveTeamMembers(teamId: string, cutoff: number): number {
|
|
147
|
+
return getTeamMemberStats(teamId, cutoff).live;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getTeamBusyState(teamId: string, cutoff: number): TeamBusyState {
|
|
151
|
+
const rows = (getDb().query(
|
|
152
|
+
"SELECT * FROM agents WHERE team_id = ? AND status = 'busy' AND last_seen > ? ORDER BY last_seen DESC, id ASC",
|
|
153
|
+
).all(teamId, cutoff) as any[])
|
|
154
|
+
.map(rowToAgent)
|
|
155
|
+
.filter((agent) => !isProviderBlocked(agent));
|
|
156
|
+
return {
|
|
157
|
+
busyMemberIds: rows.map((row) => row.id),
|
|
158
|
+
...(rows[0] ? { lastBusyAt: rows[0].lastSeen } : {}),
|
|
159
|
+
};
|
|
133
160
|
}
|
|
134
161
|
|
|
135
162
|
export function setTeamZeroMembersSince(teamId: string, ts: number): boolean {
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
} from "./constants";
|
|
52
52
|
import { emitCommand } from "./events";
|
|
53
53
|
import { reapOrphanedSessions } from "./orphaned-sessions";
|
|
54
|
-
import { reapZeroMemberTeams } from "./teams";
|
|
54
|
+
import { reapZeroMemberTeams, runTeamIdleWatchdog } from "./teams";
|
|
55
55
|
import { scanWorkspaceConflicts, workspaceGC } from "./workspaces";
|
|
56
56
|
import type { MaintenanceJobDefinition } from "./types";
|
|
57
57
|
|
|
@@ -208,6 +208,14 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
208
208
|
runOnStart: false,
|
|
209
209
|
handler: reapZeroMemberTeams,
|
|
210
210
|
},
|
|
211
|
+
{
|
|
212
|
+
id: "team-idle-watchdog",
|
|
213
|
+
title: "Team idle watchdog",
|
|
214
|
+
description: "Gently ping a team's current coordinator once when the team has had no busy members for the configured threshold.",
|
|
215
|
+
intervalMs: REAP_INTERVAL_MS,
|
|
216
|
+
runOnStart: false,
|
|
217
|
+
handler: runTeamIdleWatchdog,
|
|
218
|
+
},
|
|
211
219
|
{
|
|
212
220
|
id: "orchestrator-reaper",
|
|
213
221
|
title: "Orchestrator reaper",
|
package/src/maintenance/teams.ts
CHANGED
|
@@ -2,12 +2,128 @@ import {
|
|
|
2
2
|
clearTeamZeroMembersSince,
|
|
3
3
|
countLiveTeamMembers,
|
|
4
4
|
createActivityEvent,
|
|
5
|
+
getTeamBusyState,
|
|
6
|
+
getTeamRoster,
|
|
5
7
|
listActiveTeams,
|
|
6
8
|
setTeamZeroMembersSince,
|
|
7
9
|
} from "../db";
|
|
8
10
|
import { STALE_TTL_MS } from "../config";
|
|
11
|
+
import { agentProviderState } from "../provider-state";
|
|
9
12
|
import { dissolveTeamById } from "../services/team";
|
|
13
|
+
import { getTeamsConfig } from "../teams-config-store";
|
|
10
14
|
import { teamReapGraceMs } from "./constants";
|
|
15
|
+
import { notifyTarget } from "./events";
|
|
16
|
+
import { teamIdleTracker } from "../team-idle-tracker";
|
|
17
|
+
|
|
18
|
+
export function resetTeamIdleWatchdogForTests(): void {
|
|
19
|
+
teamIdleTracker.clear();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function teamIdlePayload(teamId: string, coordinatorId: string, thresholdMinutes: number, idleForMs: number, firstNoBusyAt: number) {
|
|
23
|
+
const roster = getTeamRoster(teamId).map((member) => {
|
|
24
|
+
const providerState = agentProviderState(member);
|
|
25
|
+
return {
|
|
26
|
+
id: member.id,
|
|
27
|
+
name: member.name,
|
|
28
|
+
label: member.label,
|
|
29
|
+
status: member.status,
|
|
30
|
+
ready: member.ready,
|
|
31
|
+
lastSeen: member.lastSeen,
|
|
32
|
+
blocked: providerState?.state === "blocked",
|
|
33
|
+
providerState: providerState ?? undefined,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
kind: "team.idle",
|
|
38
|
+
teamId,
|
|
39
|
+
coordinatorId,
|
|
40
|
+
thresholdMinutes,
|
|
41
|
+
thresholdMs: thresholdMinutes * 60_000,
|
|
42
|
+
idleForMs,
|
|
43
|
+
firstNoBusyAt,
|
|
44
|
+
roster,
|
|
45
|
+
blockedMembers: roster.filter((member) => member.blocked).map((member) => ({
|
|
46
|
+
id: member.id,
|
|
47
|
+
providerState: member.providerState,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function notifyTeamIdle(teamId: string, coordinatorId: string, thresholdMinutes: number, idleForMs: number, firstNoBusyAt: number): string | null {
|
|
53
|
+
const roundedIdleMinutes = Math.max(thresholdMinutes, Math.floor(idleForMs / 60_000));
|
|
54
|
+
const payload = teamIdlePayload(teamId, coordinatorId, thresholdMinutes, idleForMs, firstNoBusyAt);
|
|
55
|
+
return notifyTarget(
|
|
56
|
+
"team.idle",
|
|
57
|
+
{ teamId, agentId: coordinatorId },
|
|
58
|
+
coordinatorId,
|
|
59
|
+
"Team idle watchdog",
|
|
60
|
+
`Team \`${teamId}\` has had no busy members for ${roundedIdleMinutes} min. If the work is done, stand down. If not, check the team roster and restart unfinished work.`,
|
|
61
|
+
payload,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function runTeamIdleWatchdog(input: { now?: number } = {}): Record<string, unknown> {
|
|
66
|
+
const config = getTeamsConfig();
|
|
67
|
+
if (!config.enabled) {
|
|
68
|
+
const cleared = teamIdleTracker.size;
|
|
69
|
+
teamIdleTracker.clear();
|
|
70
|
+
return { enabled: false, clearedTrackedTeams: cleared };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const now = input.now ?? Date.now();
|
|
74
|
+
const cutoff = now - STALE_TTL_MS;
|
|
75
|
+
const thresholdMs = config.thresholdMinutes * 60_000;
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
const busyTeamIds: string[] = [];
|
|
78
|
+
const waitingTeamIds: string[] = [];
|
|
79
|
+
const pingedTeamIds: string[] = [];
|
|
80
|
+
const alreadyPingedTeamIds: string[] = [];
|
|
81
|
+
const missingCoordinatorTeamIds: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const team of listActiveTeams()) {
|
|
84
|
+
seen.add(team.id);
|
|
85
|
+
const busy = getTeamBusyState(team.id, cutoff);
|
|
86
|
+
if (busy.busyMemberIds.length > 0) {
|
|
87
|
+
teamIdleTracker.delete(team.id);
|
|
88
|
+
busyTeamIds.push(team.id);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entry = teamIdleTracker.get(team.id) ?? { firstNoBusyAt: now, pinged: false };
|
|
93
|
+
teamIdleTracker.set(team.id, entry);
|
|
94
|
+
const idleForMs = now - entry.firstNoBusyAt;
|
|
95
|
+
if (idleForMs < thresholdMs) {
|
|
96
|
+
waitingTeamIds.push(team.id);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (entry.pinged) {
|
|
101
|
+
alreadyPingedTeamIds.push(team.id);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!team.coordinatorId) {
|
|
105
|
+
missingCoordinatorTeamIds.push(team.id);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
notifyTeamIdle(team.id, team.coordinatorId, config.thresholdMinutes, idleForMs, entry.firstNoBusyAt);
|
|
110
|
+
entry.pinged = true;
|
|
111
|
+
pingedTeamIds.push(team.id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const key of teamIdleTracker.keys()) if (!seen.has(key)) teamIdleTracker.delete(key);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
enabled: true,
|
|
118
|
+
thresholdMinutes: config.thresholdMinutes,
|
|
119
|
+
trackedTeams: teamIdleTracker.size,
|
|
120
|
+
busyTeamIds,
|
|
121
|
+
waitingTeamIds,
|
|
122
|
+
pingedTeamIds,
|
|
123
|
+
alreadyPingedTeamIds,
|
|
124
|
+
missingCoordinatorTeamIds,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
11
127
|
|
|
12
128
|
export function reapZeroMemberTeams(): Record<string, unknown> {
|
|
13
129
|
const now = Date.now();
|
|
@@ -10,6 +10,7 @@ export type NotificationType =
|
|
|
10
10
|
| "agent.resume_budget_exhausted"
|
|
11
11
|
| "agent.transient_land_hold"
|
|
12
12
|
| "agent.rate_limit_resume"
|
|
13
|
+
| "team.idle"
|
|
13
14
|
| "plan.bound_agent_exited"
|
|
14
15
|
| "workspace.steward_task"
|
|
15
16
|
| "workspace.conflict"
|
|
@@ -60,6 +61,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
|
|
|
60
61
|
"agent.resume_budget_exhausted": null,
|
|
61
62
|
"agent.transient_land_hold": null,
|
|
62
63
|
"agent.rate_limit_resume": 15 * MINUTES,
|
|
64
|
+
"team.idle": null,
|
|
63
65
|
"plan.bound_agent_exited": null,
|
|
64
66
|
"workspace.steward_task": null,
|
|
65
67
|
"workspace.conflict": null,
|
package/src/routes/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomati
|
|
|
7
7
|
import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
|
|
8
8
|
import { getProviderQuotasRoute, postProviderQuotaLeaseAcquireRoute, postProviderQuotaLeaseReleaseRoute, postProviderQuotaRoute } from "./provider-quotas";
|
|
9
9
|
import { getProviderQuotaConfigRoute, getProviderQuotaConfigsRoute, putProviderQuotaConfigRoute } from "./provider-quota-config";
|
|
10
|
+
import { getTeamsConfigRoute, putTeamsConfigRoute } from "./teams-config";
|
|
10
11
|
import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
|
|
11
12
|
import { deleteInboxDraftRoute, getChatHistoryImportsRoute, getInboxStateRoute, patchInboxThreadState, postChatHistoryImportRoute, putInboxDraft } from "./inbox";
|
|
12
13
|
import { deleteMemoryRoute, getMemories, getMemoryBrokerInfo, getMemoryById, getMemoryStats, patchMemory, postMemory, postMemoryInject, postMemorySearch } from "./memory";
|
|
@@ -161,6 +162,8 @@ const routes: Route[] = [
|
|
|
161
162
|
route("GET", "/api/merges/pauses/:id", getMergePauseById),
|
|
162
163
|
|
|
163
164
|
route("POST", "/api/system/broadcast", postSystemBroadcast),
|
|
165
|
+
route("GET", "/api/teams/config", getTeamsConfigRoute),
|
|
166
|
+
route("PUT", "/api/teams/config", putTeamsConfigRoute),
|
|
164
167
|
route("GET", "/api/teams", getTeamsRoute),
|
|
165
168
|
route("GET", "/api/teams/outcomes", getTeamOutcomesRoute),
|
|
166
169
|
route("GET", "/api/teams/me", getMyTeamRoute),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getTeamsConfig, setTeamsConfig } from "../teams-config-store";
|
|
2
|
+
import { cleanString } from "../validation";
|
|
3
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
4
|
+
import { isRecord } from "agent-relay-sdk";
|
|
5
|
+
|
|
6
|
+
export const getTeamsConfigRoute: Handler = () => {
|
|
7
|
+
return json(getTeamsConfig());
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const putTeamsConfigRoute: Handler = async (req) => {
|
|
11
|
+
const parsed = await parseBody<unknown>(req);
|
|
12
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
13
|
+
const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
|
|
14
|
+
return json(setTeamsConfig(parsed.body, { updatedBy }));
|
|
15
|
+
};
|
package/src/routes/teams.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
2
2
|
import { authContextFromRequest } from "../services/auth-context";
|
|
3
|
-
import { getTeam, getTeamOutcomeAggregates, listAllTeams, ValidationError } from "../db";
|
|
4
|
-
import { dissolveCallerTeam, dissolveTeamById, getCallerTeam, setCallerTeamBrief, TeamAuthError, TeamNotFoundError, type TeamDissolveServiceInput } from "../services/team";
|
|
3
|
+
import { getTeam, getTeamOutcomeAggregates, getPlanByTeam, listAllTeams, ValidationError } from "../db";
|
|
4
|
+
import { dissolveCallerTeam, dissolveTeamById, deriveTeamRuntimeState, getCallerTeam, setCallerTeamBrief, TeamAuthError, TeamNotFoundError, type TeamDissolveServiceInput } from "../services/team";
|
|
5
5
|
import { cleanString, optionalEnum } from "../validation";
|
|
6
6
|
import { authorizeRoute, error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
|
|
7
|
+
import { STALE_TTL_MS } from "../config";
|
|
7
8
|
import type { Team } from "../types";
|
|
8
9
|
|
|
9
10
|
const OUTCOME_STATUSES = ["success", "partial", "failed"] as const;
|
|
@@ -43,8 +44,14 @@ export const getTeamsRoute: Handler = (req) => {
|
|
|
43
44
|
if (denied) return denied;
|
|
44
45
|
try {
|
|
45
46
|
const status = optionalEnum(new URL(req.url).searchParams.get("status") ?? undefined, "status", TEAM_STATUSES) as Team["status"] | undefined;
|
|
46
|
-
const
|
|
47
|
-
|
|
47
|
+
const cutoff = Date.now() - STALE_TTL_MS;
|
|
48
|
+
const all = listAllTeams();
|
|
49
|
+
const filtered = status ? all.filter((team) => team.status === status) : all;
|
|
50
|
+
const teams = filtered.map((team) => {
|
|
51
|
+
const plan = team.status === "active" ? getPlanByTeam(team.id) ?? null : null;
|
|
52
|
+
return { ...team, runtimeState: deriveTeamRuntimeState(team, plan, cutoff) };
|
|
53
|
+
});
|
|
54
|
+
return json({ teams });
|
|
48
55
|
} catch (err) {
|
|
49
56
|
return routeError(err);
|
|
50
57
|
}
|
package/src/security.ts
CHANGED
|
@@ -194,6 +194,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
194
194
|
if (pathname.startsWith("/api/maintenance")) return "system:admin";
|
|
195
195
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
196
196
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
197
|
+
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
197
198
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
198
199
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
199
200
|
if (pathname.startsWith("/api/issue-associations")) return method === "GET" ? "issue:read" : "issue:write";
|
|
@@ -280,6 +281,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
280
281
|
if (pathname === "/api/providers/quota-config" || pathname.match(/^\/api\/providers\/[^/]+\/quota-config$/)) {
|
|
281
282
|
return method === "GET" ? "agent:read" : "settings:write:provider";
|
|
282
283
|
}
|
|
284
|
+
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
283
285
|
if (pathname.startsWith("/api/messages") || pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
|
|
284
286
|
if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
|
|
285
287
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
@@ -304,7 +306,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
304
306
|
return "system:admin";
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notifications", "workspace", "landing", "provider"]);
|
|
309
|
+
const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notifications", "workspace", "landing", "provider", "teams"]);
|
|
308
310
|
|
|
309
311
|
function requiredSettingsScopeFor(method: string, pathname: string): string | null | undefined {
|
|
310
312
|
if (pathname === "/api/settings") return method === "GET" ? "settings:read" : null;
|
package/src/services/team.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { hasScope } from "agent-relay-sdk/types";
|
|
2
2
|
import { STALE_TTL_MS } from "../config";
|
|
3
|
-
import { claimTeam, countLiveTeamMembers, dissolveTeam, ensureTeam, getAgent, getPlanByTeam, getTeam, getTeamRoster, setTeamBrief } from "../db";
|
|
3
|
+
import { claimTeam, countLiveTeamMembers, dissolveTeam, ensureTeam, getAgent, getPlanByTeam, getTeam, getTeamBusyState, getTeamMemberStats, getTeamRoster, setTeamBrief } from "../db";
|
|
4
4
|
import { emitRelayEvent } from "../events";
|
|
5
5
|
import { listIssueRefsForEntity } from "./issue-associations";
|
|
6
|
+
import { getTeamIdleEntry } from "../team-idle-tracker";
|
|
6
7
|
import type { AuthContext } from "./auth-context";
|
|
7
|
-
import type { Team } from "../types";
|
|
8
|
+
import type { Plan, Team, TeamRuntimeState } from "../types";
|
|
8
9
|
|
|
9
10
|
export class TeamAuthError extends Error {}
|
|
10
11
|
export class TeamNotFoundError extends Error {}
|
|
@@ -30,6 +31,16 @@ export interface TeamDissolveServiceInput {
|
|
|
30
31
|
landsClean?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export function deriveTeamRuntimeState(team: Team, plan: Plan | null, cutoff: number): TeamRuntimeState {
|
|
35
|
+
if (team.status === "dissolved") return "dissolved";
|
|
36
|
+
const busy = getTeamBusyState(team.id, cutoff);
|
|
37
|
+
if (busy.busyMemberIds.length > 0) return "busy";
|
|
38
|
+
if (getTeamIdleEntry(team.id)?.pinged) return "stalled";
|
|
39
|
+
const stats = getTeamMemberStats(team.id, cutoff);
|
|
40
|
+
if (stats.total > stats.live && plan && plan.nodes.some((n) => n.status !== "landed")) return "degraded";
|
|
41
|
+
return "idle";
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
function requireCallerAgentId(ctx: AuthContext): string {
|
|
34
45
|
if (!ctx.callerAgentId) throw new TeamAuthError("team operations require a single caller agent identity");
|
|
35
46
|
return ctx.callerAgentId;
|
|
@@ -39,12 +50,14 @@ function payloadForAgent(agentId: string, channel?: string): TeamPayload {
|
|
|
39
50
|
const agent = getAgent(agentId);
|
|
40
51
|
const team = agent?.teamId ? getTeam(agent.teamId) : null;
|
|
41
52
|
if (!team) throw new TeamNotFoundError("caller has no team");
|
|
53
|
+
const plan = getPlanByTeam(team.id, channel) ?? null;
|
|
54
|
+
const runtimeState = deriveTeamRuntimeState(team, plan, Date.now() - STALE_TTL_MS);
|
|
42
55
|
return {
|
|
43
|
-
team,
|
|
56
|
+
team: { ...team, runtimeState },
|
|
44
57
|
roster: getTeamRoster(team.id),
|
|
45
58
|
brief: team.brief ?? null,
|
|
46
59
|
definitionOfDone: team.definitionOfDone ?? null,
|
|
47
|
-
plan
|
|
60
|
+
plan,
|
|
48
61
|
issues: listIssueRefsForEntity("team", team.id),
|
|
49
62
|
};
|
|
50
63
|
}
|
|
@@ -4,6 +4,7 @@ import { createActivityEvent, getAgent, listAgents, listWorkspaces } from "../db
|
|
|
4
4
|
import { routeNotification } from "../notification-router";
|
|
5
5
|
import { isProviderBlocked } from "../provider-state";
|
|
6
6
|
import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
|
|
7
|
+
import type { WorkspaceRecord } from "../types";
|
|
7
8
|
|
|
8
9
|
const envMsOrDefault = (name: string, fallback: number): number => {
|
|
9
10
|
const v = Number(process.env[name]);
|
|
@@ -17,6 +18,34 @@ export function resetTransientTrackerForTests(): void {
|
|
|
17
18
|
transientTracker.clear();
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// How many commits the agent's branch has ahead of its base. Returns null when
|
|
22
|
+
// the check can't run (missing fields, git failure) — callers treat null as
|
|
23
|
+
// "unknown" and preserve the safe #614 hold-pending bias: only 0 is fast-reaped.
|
|
24
|
+
function defaultCommitsAhead(ws: WorkspaceRecord): number | null {
|
|
25
|
+
const base = ws.baseSha ?? ws.baseRef;
|
|
26
|
+
if (!base || !ws.branch || !ws.worktreePath) return null;
|
|
27
|
+
try {
|
|
28
|
+
const proc = Bun.spawnSync(
|
|
29
|
+
["git", "-C", ws.worktreePath, "rev-list", "--count", `${base}..${ws.branch}`],
|
|
30
|
+
{ stdin: "ignore", stdout: "pipe", stderr: "ignore" },
|
|
31
|
+
);
|
|
32
|
+
if (proc.exitCode !== 0) return null;
|
|
33
|
+
const n = parseInt(Buffer.from(proc.stdout).toString("utf8").trim(), 10);
|
|
34
|
+
return Number.isFinite(n) ? n : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let _commitsAheadFn: (ws: WorkspaceRecord) => number | null = defaultCommitsAhead;
|
|
41
|
+
|
|
42
|
+
export function overrideCommitsAheadForTests(fn: (ws: WorkspaceRecord) => number | null): void {
|
|
43
|
+
_commitsAheadFn = fn;
|
|
44
|
+
}
|
|
45
|
+
export function resetCommitsAheadForTests(): void {
|
|
46
|
+
_commitsAheadFn = defaultCommitsAhead;
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
function transientLandComplete(agentId: string): { complete: boolean; workspaceId?: string; status?: string; reason?: string } {
|
|
21
50
|
const isolated = listWorkspaces({ ownerAgentId: agentId }).filter((ws) => ws.mode === "isolated");
|
|
22
51
|
if (!isolated.length) return { complete: true, reason: "no-isolated-workspace" };
|
|
@@ -27,6 +56,13 @@ function transientLandComplete(agentId: string): { complete: boolean; workspaceI
|
|
|
27
56
|
if (live.status === "active" && mergedAt !== undefined && mergedAt > (landReportedAt ?? 0)) {
|
|
28
57
|
return { complete: true, workspaceId: live.id, status: live.status, reason: "recycled-after-land" };
|
|
29
58
|
}
|
|
59
|
+
// Fast-path (#629): a zero-commit branch has nothing to preserve — reap immediately
|
|
60
|
+
// instead of holding it pending a land that will never happen. Only fires when the
|
|
61
|
+
// branch is provably empty (count === 0); null (unknown/git error) falls through to
|
|
62
|
+
// the safe #614 hold-pending bias below.
|
|
63
|
+
if (live.status === "active" && _commitsAheadFn(live) === 0) {
|
|
64
|
+
return { complete: true, workspaceId: live.id, status: live.status, reason: "empty-branch" };
|
|
65
|
+
}
|
|
30
66
|
return { complete: false, workspaceId: live.id, status: live.status, reason: "land-pending" };
|
|
31
67
|
}
|
|
32
68
|
|
package/src/settings/registry.ts
CHANGED
|
@@ -20,11 +20,14 @@ import {
|
|
|
20
20
|
APPROVAL_MODES,
|
|
21
21
|
errMessage,
|
|
22
22
|
SPAWN_PROVIDERS,
|
|
23
|
+
TEAM_IDLE_THRESHOLD_MINUTES_MAX,
|
|
24
|
+
TEAM_IDLE_THRESHOLD_MINUTES_MIN,
|
|
23
25
|
VALID_EFFORTS,
|
|
24
26
|
type JSONSchema,
|
|
25
27
|
type SettingDescriptor,
|
|
26
28
|
type SettingScope,
|
|
27
29
|
} from "agent-relay-sdk";
|
|
30
|
+
import { DEFAULT_TEAMS_CONFIG } from "agent-relay-sdk/teams-config";
|
|
28
31
|
|
|
29
32
|
/** Fail-closed default scope — a descriptor that omits scope requires admin to
|
|
30
33
|
* read and write (#385 owns the scope grammar; this layer only declares it). */
|
|
@@ -215,6 +218,20 @@ const WORKSPACE_SCHEMA: JSONSchema = {
|
|
|
215
218
|
},
|
|
216
219
|
};
|
|
217
220
|
|
|
221
|
+
const TEAMS_SCHEMA: JSONSchema = {
|
|
222
|
+
type: "object",
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
required: ["enabled", "thresholdMinutes"],
|
|
225
|
+
properties: {
|
|
226
|
+
enabled: { type: "boolean" },
|
|
227
|
+
thresholdMinutes: {
|
|
228
|
+
type: "integer",
|
|
229
|
+
minimum: TEAM_IDLE_THRESHOLD_MINUTES_MIN,
|
|
230
|
+
maximum: TEAM_IDLE_THRESHOLD_MINUTES_MAX,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
218
235
|
const LANDING_SCHEMA: JSONSchema = {
|
|
219
236
|
type: "object",
|
|
220
237
|
additionalProperties: false,
|
|
@@ -347,6 +364,16 @@ function seedBuiltInSettings(): void {
|
|
|
347
364
|
display: { group: "Workspace", label: "Landing strategy", description: "Default land strategy for workspace branches.", order: 41 },
|
|
348
365
|
});
|
|
349
366
|
|
|
367
|
+
registerSetting({
|
|
368
|
+
namespace: "teams",
|
|
369
|
+
key: "default",
|
|
370
|
+
schema: TEAMS_SCHEMA,
|
|
371
|
+
defaults: DEFAULT_TEAMS_CONFIG,
|
|
372
|
+
scope: { read: "settings:read:teams", write: "settings:write:teams" },
|
|
373
|
+
tier: "server-runtime",
|
|
374
|
+
display: { group: "Teams", label: "Team idle watchdog", description: "Coordinator nudge when a team has no busy members.", order: 45 },
|
|
375
|
+
});
|
|
376
|
+
|
|
350
377
|
registerSetting({
|
|
351
378
|
namespace: "provider",
|
|
352
379
|
key: "chatCaptureMode",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Singleton in-memory tracker shared by maintenance/teams.ts (writer) and
|
|
2
|
+
// services/team.ts (reader). Lives in its own module to avoid a circular dep:
|
|
3
|
+
// maintenance/teams.ts → services/team (dissolveTeamById) → team-idle-tracker ← maintenance/teams.ts
|
|
4
|
+
export interface TeamIdleEntry {
|
|
5
|
+
firstNoBusyAt: number;
|
|
6
|
+
pinged: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const teamIdleTracker = new Map<string, TeamIdleEntry>();
|
|
10
|
+
|
|
11
|
+
export function getTeamIdleEntry(teamId: string): TeamIdleEntry | undefined {
|
|
12
|
+
return teamIdleTracker.get(teamId);
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getConfig, setConfig } from "./config-store";
|
|
2
|
+
import { emitConfigChanged } from "./sse";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TEAMS_CONFIG,
|
|
5
|
+
normalizeTeamsConfig,
|
|
6
|
+
type TeamsConfig,
|
|
7
|
+
} from "agent-relay-sdk";
|
|
8
|
+
|
|
9
|
+
const TEAMS_CONFIG_NAMESPACE = "teams";
|
|
10
|
+
const TEAMS_CONFIG_KEY = "default";
|
|
11
|
+
|
|
12
|
+
export function getTeamsConfig(): TeamsConfig {
|
|
13
|
+
const entry = getConfig<TeamsConfig>(TEAMS_CONFIG_NAMESPACE, TEAMS_CONFIG_KEY);
|
|
14
|
+
return entry ? normalizeTeamsConfig(entry.value) : { ...DEFAULT_TEAMS_CONFIG };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setTeamsConfig(value: unknown, opts: { updatedBy?: string } = {}): TeamsConfig {
|
|
18
|
+
const normalized = normalizeTeamsConfig(value);
|
|
19
|
+
const entry = setConfig(TEAMS_CONFIG_NAMESPACE, TEAMS_CONFIG_KEY, normalized, opts.updatedBy);
|
|
20
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|