agent-relay-server 0.86.0 → 0.87.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 (63) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +1 -1
  3. package/public/assets/{MessageBubble-CevL-2jc.js → MessageBubble-DS4Hbn0k.js} +2 -2
  4. package/public/assets/{MessageBubble-CevL-2jc.js.map → MessageBubble-DS4Hbn0k.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-DGpJhuVw.js → PlanGraphPanel-BYlragEi.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-DGpJhuVw.js.map → PlanGraphPanel-BYlragEi.js.map} +1 -1
  7. package/public/assets/{activity-CY2Cbpet.js → activity-CcGFS_sY.js} +2 -2
  8. package/public/assets/{activity-CY2Cbpet.js.map → activity-CcGFS_sY.js.map} +1 -1
  9. package/public/assets/{agents-Bj5KzrYJ.js → agents-BTh8SQiP.js} +2 -2
  10. package/public/assets/{agents-Bj5KzrYJ.js.map → agents-BTh8SQiP.js.map} +1 -1
  11. package/public/assets/{automation-BOPk1oh_.js → automation-izYGpZyQ.js} +2 -2
  12. package/public/assets/{automation-BOPk1oh_.js.map → automation-izYGpZyQ.js.map} +1 -1
  13. package/public/assets/{chat-D5pI4ebq.js → chat-ThyPRUPq.js} +2 -2
  14. package/public/assets/{chat-D5pI4ebq.js.map → chat-ThyPRUPq.js.map} +1 -1
  15. package/public/assets/{formatted-body-impl-Dr_z1UvS.js → formatted-body-impl-DEYxGoiw.js} +2 -2
  16. package/public/assets/{formatted-body-impl-Dr_z1UvS.js.map → formatted-body-impl-DEYxGoiw.js.map} +1 -1
  17. package/public/assets/index-Bd1NBLyd.js +23 -0
  18. package/public/assets/{index-CEXhh5nz.js.map → index-Bd1NBLyd.js.map} +1 -1
  19. package/public/assets/index-BjbvhfFt.css +2 -0
  20. package/public/assets/{insights-C4dEK49e.js → insights-DAVKQ_SF.js} +2 -2
  21. package/public/assets/{insights-C4dEK49e.js.map → insights-DAVKQ_SF.js.map} +1 -1
  22. package/public/assets/{maintenance-DJZUSsja.js → maintenance-BL4R9076.js} +2 -2
  23. package/public/assets/{maintenance-DJZUSsja.js.map → maintenance-BL4R9076.js.map} +1 -1
  24. package/public/assets/{managed-agents-5wUMDKVi.js → managed-agents-C7n1gl7E.js} +2 -2
  25. package/public/assets/{managed-agents-5wUMDKVi.js.map → managed-agents-C7n1gl7E.js.map} +1 -1
  26. package/public/assets/{markdown-preview-impl-DOJQR3cO.js → markdown-preview-impl-DK0wNWXJ.js} +2 -2
  27. package/public/assets/{markdown-preview-impl-DOJQR3cO.js.map → markdown-preview-impl-DK0wNWXJ.js.map} +1 -1
  28. package/public/assets/{memory-BnhAD09c.js → memory-BJEkDkwl.js} +2 -2
  29. package/public/assets/{memory-BnhAD09c.js.map → memory-BJEkDkwl.js.map} +1 -1
  30. package/public/assets/{messages-Cc9YF8Wl.js → messages-DBn9tXx2.js} +2 -2
  31. package/public/assets/{messages-Cc9YF8Wl.js.map → messages-DBn9tXx2.js.map} +1 -1
  32. package/public/assets/{orchestrators-B01iyiBA.js → orchestrators-Bc1eC16Q.js} +2 -2
  33. package/public/assets/{orchestrators-B01iyiBA.js.map → orchestrators-Bc1eC16Q.js.map} +1 -1
  34. package/public/assets/{overview-DN8CUTVx.js → overview-Bw7zrSqR.js} +2 -2
  35. package/public/assets/{overview-DN8CUTVx.js.map → overview-Bw7zrSqR.js.map} +1 -1
  36. package/public/assets/{pairs-BxuDMLhs.js → pairs-BSwWnLK2.js} +2 -2
  37. package/public/assets/{pairs-BxuDMLhs.js.map → pairs-BSwWnLK2.js.map} +1 -1
  38. package/public/assets/{security-CO0PBIau.js → security-wTBapq4K.js} +2 -2
  39. package/public/assets/{security-CO0PBIau.js.map → security-wTBapq4K.js.map} +1 -1
  40. package/public/assets/{select-DHaFsLvt.js → select-D6HFOeLW.js} +2 -2
  41. package/public/assets/{select-DHaFsLvt.js.map → select-D6HFOeLW.js.map} +1 -1
  42. package/public/assets/{settings-De1DKwd6.js → settings-CDzp7vqJ.js} +2 -2
  43. package/public/assets/{settings-De1DKwd6.js.map → settings-CDzp7vqJ.js.map} +1 -1
  44. package/public/assets/{tasks-Bqd188ql.js → tasks-Bd7Wjy6e.js} +2 -2
  45. package/public/assets/{tasks-Bqd188ql.js.map → tasks-Bd7Wjy6e.js.map} +1 -1
  46. package/public/assets/{teams-Mh6ypDPr.js → teams-l3GUr2XV.js} +2 -2
  47. package/public/assets/{teams-Mh6ypDPr.js.map → teams-l3GUr2XV.js.map} +1 -1
  48. package/public/assets/{terminal-viewer-impl-B7x_JNZe.js → terminal-viewer-impl-BQMMWctd.js} +2 -2
  49. package/public/assets/{terminal-viewer-impl-B7x_JNZe.js.map → terminal-viewer-impl-BQMMWctd.js.map} +1 -1
  50. package/public/assets/{user-avatar-Dy5J3E_b.js → user-avatar-DcA69UJK.js} +2 -2
  51. package/public/assets/{user-avatar-Dy5J3E_b.js.map → user-avatar-DcA69UJK.js.map} +1 -1
  52. package/public/assets/{work-queue-ClGgOUSu.js → work-queue-BYlFeJ1g.js} +2 -2
  53. package/public/assets/{work-queue-ClGgOUSu.js.map → work-queue-BYlFeJ1g.js.map} +1 -1
  54. package/public/index.html +2 -2
  55. package/src/agent-busy-health.ts +93 -0
  56. package/src/agent-card-projection.ts +4 -1
  57. package/src/config.ts +5 -0
  58. package/src/maintenance/jobs.ts +75 -0
  59. package/src/maintenance/teams.ts +108 -2
  60. package/src/notification-types.ts +4 -0
  61. package/src/team-idle-tracker.ts +19 -0
  62. package/public/assets/index-CEXhh5nz.js +0 -23
  63. package/public/assets/index-LUIvWcx6.css +0 -2
@@ -1,2 +1,2 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{C as s}from"./index-CEXhh5nz.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
- //# sourceMappingURL=user-avatar-Dy5J3E_b.js.map
1
+ import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{C as s}from"./index-Bd1NBLyd.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
+ //# sourceMappingURL=user-avatar-DcA69UJK.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar-Dy5J3E_b.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
1
+ {"version":3,"file":"user-avatar-DcA69UJK.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
@@ -1,2 +1,2 @@
1
- import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DsqUEZOf.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{B as f,K as p,s as m}from"./index-CEXhh5nz.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children: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:i,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)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,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)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-ClGgOUSu.js.map
1
+ import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DsqUEZOf.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{B as f,K as p,s as m}from"./index-Bd1NBLyd.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children: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:i,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)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,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)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-BYlFeJ1g.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-ClGgOUSu.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-BYlFeJ1g.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-CEXhh5nz.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-Bd1NBLyd.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-CsPXvXMC.js">
@@ -37,7 +37,7 @@
37
37
  <link rel="modulepreload" crossorigin href="./assets/badge-CK6ejiOh.js">
38
38
  <link rel="modulepreload" crossorigin href="./assets/input-CLW2gDUM.js">
39
39
  <link rel="modulepreload" crossorigin href="./assets/themes-mW9zZP4O.js">
40
- <link rel="stylesheet" crossorigin href="./assets/index-LUIvWcx6.css">
40
+ <link rel="stylesheet" crossorigin href="./assets/index-BjbvhfFt.css">
41
41
  </head>
42
42
  <body class="bg-background text-foreground antialiased">
43
43
  <div id="root"></div>
@@ -0,0 +1,93 @@
1
+ import {
2
+ STUCK_BUSY_PROVIDER_TURN_MS,
3
+ STUCK_BUSY_RECONNECT_MS,
4
+ STUCK_BUSY_SHUTDOWN_MS,
5
+ STUCK_BUSY_STARTUP_MS,
6
+ } from "./config";
7
+ import { stringValue } from "agent-relay-sdk";
8
+ import type { AgentCard } from "./types";
9
+
10
+ type BusyHealthKind = "provider-turn" | "startup" | "reconnect" | "shutdown";
11
+
12
+ interface ActiveWorkLike {
13
+ id?: unknown;
14
+ kind?: unknown;
15
+ label?: unknown;
16
+ startedAt?: unknown;
17
+ }
18
+
19
+ export interface AgentBusyHealth {
20
+ status: "ok" | "stuck-busy";
21
+ kind: BusyHealthKind;
22
+ thresholdMs: number;
23
+ activeWorkId?: string;
24
+ activeWorkKind?: string;
25
+ activeWorkLabel?: string;
26
+ activeWorkStartedAt: number;
27
+ activeWorkAgeMs: number;
28
+ lastProviderEventAt?: number;
29
+ lastProviderEventAgeMs?: number;
30
+ staleForMs: number;
31
+ }
32
+
33
+ function numberValue(value: unknown): number | undefined {
34
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
35
+ }
36
+
37
+ function activeWork(agent: AgentCard): ActiveWorkLike[] {
38
+ return Array.isArray(agent.meta?.activeWork) ? agent.meta.activeWork as ActiveWorkLike[] : [];
39
+ }
40
+
41
+ function thresholdFor(kind: BusyHealthKind): number {
42
+ if (kind === "startup") return STUCK_BUSY_STARTUP_MS;
43
+ if (kind === "reconnect") return STUCK_BUSY_RECONNECT_MS;
44
+ if (kind === "shutdown") return STUCK_BUSY_SHUTDOWN_MS;
45
+ return STUCK_BUSY_PROVIDER_TURN_MS;
46
+ }
47
+
48
+ function classifyBusy(agent: AgentCard, now: number): { kind: BusyHealthKind; work?: ActiveWorkLike; startedAt: number } | null {
49
+ if (agent.status !== "busy") return null;
50
+ const works = activeWork(agent);
51
+ const providerTurn = works.find((item) => item.kind === "provider-turn");
52
+ if (providerTurn) {
53
+ return { kind: "provider-turn", work: providerTurn, startedAt: numberValue(providerTurn.startedAt) ?? agent.lastSeen };
54
+ }
55
+
56
+ const lifecycleAction = stringValue(agent.meta?.lifecycleAction) ?? "";
57
+ const lifecycleActionAt = numberValue(agent.meta?.lifecycleActionAt);
58
+ if (lifecycleAction.includes("shutdown") || lifecycleAction === "killing") {
59
+ return { kind: "shutdown", work: works[0], startedAt: lifecycleActionAt ?? numberValue(works[0]?.startedAt) ?? agent.lastSeen };
60
+ }
61
+ if (lifecycleAction.includes("reconnect") || lifecycleAction === "resuming") {
62
+ return { kind: "reconnect", work: works[0], startedAt: lifecycleActionAt ?? numberValue(works[0]?.startedAt) ?? agent.lastSeen };
63
+ }
64
+ if (!agent.ready) {
65
+ return { kind: "startup", work: works[0], startedAt: numberValue(agent.meta?.startedAt) ?? agent.createdAt };
66
+ }
67
+ return works[0] ? { kind: "provider-turn", work: works[0], startedAt: numberValue(works[0].startedAt) ?? agent.lastSeen } : null;
68
+ }
69
+
70
+ export function deriveAgentBusyHealth(agent: AgentCard, now: number = Date.now()): AgentBusyHealth | undefined {
71
+ const classified = classifyBusy(agent, now);
72
+ if (!classified) return undefined;
73
+ const thresholdMs = thresholdFor(classified.kind);
74
+ const activeWorkStartedAt = Math.max(0, classified.startedAt);
75
+ const activeWorkAgeMs = Math.max(0, now - activeWorkStartedAt);
76
+ const lastProviderEventAt = numberValue(agent.meta?.lastProviderEventAt);
77
+ const lastProgressAt = lastProviderEventAt ?? activeWorkStartedAt;
78
+ const staleForMs = Math.max(0, now - lastProgressAt);
79
+ const stuck = activeWorkAgeMs >= thresholdMs && staleForMs >= thresholdMs;
80
+ const work = classified.work;
81
+ return {
82
+ status: stuck ? "stuck-busy" : "ok",
83
+ kind: classified.kind,
84
+ thresholdMs,
85
+ ...(stringValue(work?.id) ? { activeWorkId: stringValue(work?.id) } : {}),
86
+ ...(stringValue(work?.kind) ? { activeWorkKind: stringValue(work?.kind) } : {}),
87
+ ...(stringValue(work?.label) ? { activeWorkLabel: stringValue(work?.label) } : {}),
88
+ activeWorkStartedAt,
89
+ activeWorkAgeMs,
90
+ ...(lastProviderEventAt !== undefined ? { lastProviderEventAt, lastProviderEventAgeMs: Math.max(0, now - lastProviderEventAt) } : {}),
91
+ staleForMs,
92
+ };
93
+ }
@@ -10,12 +10,15 @@
10
10
  // surface; a new emitter can't reintroduce the drift without going around this function.
11
11
 
12
12
  import { attachBranchState } from "./agent-branch-state";
13
+ import { deriveAgentBusyHealth } from "./agent-busy-health";
13
14
  import { attachIssueRepo } from "./agent-issue-repo";
14
15
  import type { AgentCard } from "./types";
15
16
 
16
17
  /** Enrich an AgentCard with the derived fields the dashboard expects on every surface. */
17
18
  export function projectAgentCard(agent: AgentCard): AgentCard {
18
- return attachIssueRepo(attachBranchState(agent));
19
+ const projected = attachIssueRepo(attachBranchState(agent));
20
+ const busyHealth = deriveAgentBusyHealth(projected);
21
+ return busyHealth ? { ...projected, meta: { ...(projected.meta ?? {}), busyHealth } } : projected;
19
22
  }
20
23
 
21
24
  /** The `agent.status` SSE payload: the enriched card when the agent is known, else the
package/src/config.ts CHANGED
@@ -59,6 +59,11 @@ export const SPAWN_REGISTRATION_NOTIFY_MAX_AGE_MS = envPositiveInt("AGENT_RELAY_
59
59
  export const DEFAULT_FANOUT_TTL_SECONDS = envPositiveInt("AGENT_RELAY_FANOUT_TTL_SECONDS", 2 * 60 * 60); // 2h
60
60
  export const OFFLINE_PRUNE_MS = envPositiveInt("OFFLINE_PRUNE_MS", DAY_MS); // 24h offline → delete
61
61
  export const REAP_INTERVAL_MS = envPositiveInt("REAP_INTERVAL_MS", 60_000); // reaper cadence
62
+ export const STUCK_BUSY_THRESHOLD_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_THRESHOLD_MS", 15 * 60_000);
63
+ export const STUCK_BUSY_PROVIDER_TURN_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_PROVIDER_TURN_MS", STUCK_BUSY_THRESHOLD_MS);
64
+ export const STUCK_BUSY_STARTUP_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_STARTUP_MS", STUCK_BUSY_THRESHOLD_MS);
65
+ export const STUCK_BUSY_RECONNECT_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_RECONNECT_MS", STUCK_BUSY_THRESHOLD_MS);
66
+ export const STUCK_BUSY_SHUTDOWN_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_SHUTDOWN_MS", STUCK_BUSY_THRESHOLD_MS);
62
67
  export const CLAIM_LEASE_MS = envPositiveInt("AGENT_RELAY_CLAIM_LEASE_MS", 1_800_000); // 30min claim lease
63
68
  export const POOL_CLAIM_LEASE_MS = envPositiveInt("AGENT_RELAY_POOL_CLAIM_LEASE_MS", STALE_TTL_MS * 3); // pool binding lease
64
69
  // Per-repo merge serialization lease — only one base merge may run at a time per
@@ -1,4 +1,5 @@
1
1
  import { getArtifactStorage } from "../artifact-storage";
2
+ import { deriveAgentBusyHealth } from "../agent-busy-health";
2
3
  import { expireStaleBusAgents } from "../bus";
3
4
  import { pruneOutbox } from "../bus-outbox";
4
5
  import { expireCommands } from "../commands-db";
@@ -8,6 +9,7 @@ import {
8
9
  evaluatePoolBindings,
9
10
  expireQueuedMessages,
10
11
  getAgent,
12
+ listAgents,
11
13
  pruneOrphanedSharedWorkspaces,
12
14
  pruneOfflineAgents,
13
15
  pruneOldMessages,
@@ -35,6 +37,7 @@ import {
35
37
  emitPoolBindingChanged,
36
38
  emitTaskChanged,
37
39
  } from "../sse";
40
+ import { routeNotification } from "../notification-router";
38
41
  import { pruneExpiredTokenRecords } from "../token-db";
39
42
  import { reapTransientAgents } from "../services/transient-agent-reaper";
40
43
  import { reapUnregisteredSpawnCommands } from "../services/spawn-registration-watchdog";
@@ -63,6 +66,70 @@ import type { MaintenanceJobDefinition } from "./types";
63
66
 
64
67
  let dbMaintenanceRuns = 0;
65
68
 
69
+ function isManagedBusyWatchCandidate(agent: ReturnType<typeof listAgents>[number]): boolean {
70
+ if (agent.kind !== "provider") return false;
71
+ if (agent.status !== "busy") return false;
72
+ if (agent.id === "user" || agent.id === "system") return false;
73
+ return agent.meta?.runnerManaged === true ||
74
+ typeof agent.meta?.policyName === "string" ||
75
+ agent.providerCapabilities?.lifecycle?.managed === true;
76
+ }
77
+
78
+ function stuckBusyBody(agentId: string, health: NonNullable<ReturnType<typeof deriveAgentBusyHealth>>): string {
79
+ const staleMinutes = Math.max(1, Math.floor(health.staleForMs / 60_000));
80
+ const work = health.activeWorkLabel || health.activeWorkKind || health.activeWorkId || health.kind;
81
+ return `Agent \`${agentId}\` appears stuck busy in ${work} for ${staleMinutes} min with no provider progress. Use interrupt first if available; restart or shutdown if the turn is wedged.`;
82
+ }
83
+
84
+ export function runStuckBusyWatchdog(input: { now?: number } = {}): Record<string, unknown> {
85
+ const now = input.now ?? Date.now();
86
+ const stuckAgentIds: string[] = [];
87
+ const notifiedAgentIds: string[] = [];
88
+ const checkedAgentIds: string[] = [];
89
+
90
+ for (const agent of listAgents({ status: "busy" })) {
91
+ if (!isManagedBusyWatchCandidate(agent)) continue;
92
+ checkedAgentIds.push(agent.id);
93
+ const health = deriveAgentBusyHealth(agent, now);
94
+ if (health?.status !== "stuck-busy") continue;
95
+ stuckAgentIds.push(agent.id);
96
+
97
+ const idempotencyKey = `agent-stuck-busy:${agent.id}:${health.kind}:${health.activeWorkStartedAt}:${health.thresholdMs}`;
98
+ createActivityEvent({
99
+ clientId: idempotencyKey,
100
+ kind: "state",
101
+ title: "Agent stuck busy",
102
+ body: stuckBusyBody(agent.id, health),
103
+ icon: "ti-alert-triangle",
104
+ view: "agents",
105
+ agentId: agent.id,
106
+ metadata: {
107
+ source: "server",
108
+ maintenanceJobId: "stuck-busy-watchdog",
109
+ severity: "warning",
110
+ busyHealth: health,
111
+ },
112
+ });
113
+
114
+ const messages = routeNotification({
115
+ type: "agent.stuck_busy",
116
+ scope: { agentId: agent.id, parent: agent.spawnedBy, teamId: agent.teamId },
117
+ recipients: ["user"],
118
+ message: {
119
+ subject: "Agent stuck busy",
120
+ body: stuckBusyBody(agent.id, health),
121
+ payload: { kind: "agent.stuck_busy", agentId: agent.id, busyHealth: health },
122
+ idempotencyKey,
123
+ replyExpected: false,
124
+ },
125
+ });
126
+ if (messages.length > 0) notifiedAgentIds.push(agent.id);
127
+ emitAgentStatus(agent.id);
128
+ }
129
+
130
+ return { checkedAgentIds, stuckAgentIds, notifiedAgentIds };
131
+ }
132
+
66
133
  export const LEGACY_REAPER_JOB_IDS = [
67
134
  "claim-cleanup",
68
135
  "task-orphan-release",
@@ -219,6 +286,14 @@ export const definitions: MaintenanceJobDefinition[] = [
219
286
  },
220
287
  { id: "transient-agent-reaper", title: "Transient agent reaper", description: "Cleanly shut down transient spawned agents once they are idle and any isolated branch work has landed.", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: reapTransientAgents },
221
288
  { id: "rate-limit-resume", title: "Rate-limit resume", description: "Auto-resume agents held on a provider usage/rate-limit stall once their reset window has passed (#286).", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: resumeRateLimitedAgents },
289
+ {
290
+ id: "stuck-busy-watchdog",
291
+ title: "Stuck busy watchdog",
292
+ description: "Warn when a managed provider stays busy past the threshold without provider progress (#53).",
293
+ intervalMs: REAP_INTERVAL_MS,
294
+ runOnStart: false,
295
+ handler: runStuckBusyWatchdog,
296
+ },
222
297
  {
223
298
  id: "team-reap",
224
299
  title: "Team lifecycle close-out",
@@ -2,6 +2,7 @@ import {
2
2
  clearTeamZeroMembersSince,
3
3
  countLiveTeamMembers,
4
4
  createActivityEvent,
5
+ getAgent,
5
6
  getCrashReport,
6
7
  hasTerminalEvent,
7
8
  getTeamBusyState,
@@ -9,18 +10,25 @@ import {
9
10
  listActiveTeams,
10
11
  setTeamZeroMembersSince,
11
12
  } from "../db";
12
- import { STALE_TTL_MS } from "../config";
13
+ import { STALE_TTL_MS, stewardFallbackTarget } from "../config";
13
14
  import { agentProviderState } from "../provider-state";
14
15
  import { dissolveTeamById } from "../services/team";
16
+ import { resolveCoordinatorAgentId } from "../steward-identity";
15
17
  import { getTeamsConfig } from "../teams-config-store";
16
18
  import { teamReapGraceMs } from "./constants";
17
19
  import { notifyTarget } from "./events";
18
- import { teamIdleTracker, teamMemberWatchTracker, type TeamMemberWatch } from "../team-idle-tracker";
20
+ import {
21
+ teamCoordinatorWatchTracker,
22
+ teamIdleTracker,
23
+ teamMemberWatchTracker,
24
+ type TeamMemberWatch,
25
+ } from "../team-idle-tracker";
19
26
  import type { AgentCard } from "../types";
20
27
 
21
28
  export function resetTeamIdleWatchdogForTests(): void {
22
29
  teamIdleTracker.clear();
23
30
  teamMemberWatchTracker.clear();
31
+ teamCoordinatorWatchTracker.clear();
24
32
  }
25
33
 
26
34
  /** A member counts as "alive" (worth watching for a future silence) when it's past onboarding and
@@ -84,6 +92,95 @@ function detectSilentMembers(
84
92
  }
85
93
  }
86
94
 
95
+ /**
96
+ * #412/#468 — detect a team's COORDINATOR going silent and escalate to a human, instead of the team
97
+ * stalling forever. The member watchdog above SKIPS the coordinator (it's the ping recipient), and a
98
+ * missing/dead coordinator otherwise only gets recorded — never surfaced — so a silently-dead
99
+ * coordinator means nobody is driving and no member is even being listened to (#412's single point of
100
+ * failure). Reuses the SAME aliveness predicate (`memberIsAlive`) + terminal-event suppression
101
+ * (`getCrashReport || hasTerminalEvent`) as the member watch so a CLEANLY-exited coordinator does not
102
+ * false-positive. Escalates ONCE per gone-spell (re-arms on recovery) up the lineage — the gone
103
+ * coordinator's own coordinator (#639 resolver), falling back to the configured human/steward target
104
+ * — never dead-ending at `system`. A coordinator-gone team is always surfaced, never silently
105
+ * abandoned (no-silent-dissolve).
106
+ */
107
+ function detectGoneCoordinator(
108
+ teamId: string,
109
+ coordinatorId: string | undefined,
110
+ now: number,
111
+ thresholdMs: number,
112
+ out: { escalatedTeamIds: string[]; suppressedTeamIds: string[]; goneCoordinatorIds: string[] },
113
+ ): void {
114
+ if (!coordinatorId) return; // no coordinator on the row — nothing to watch (idle path records this)
115
+ const freshCutoff = now - thresholdMs;
116
+ const existing = teamCoordinatorWatchTracker.get(teamId);
117
+ // A re-elected/claimed coordinator is a fresh identity — drop a watch left over from the old one.
118
+ if (existing && existing.coordinatorId !== coordinatorId) teamCoordinatorWatchTracker.delete(teamId);
119
+
120
+ const coord = getAgent(coordinatorId);
121
+ if (coord && memberIsAlive(coord, freshCutoff)) {
122
+ const prior = existing?.coordinatorId === coordinatorId ? existing : undefined;
123
+ teamCoordinatorWatchTracker.set(teamId, {
124
+ coordinatorId,
125
+ lastActiveAt: Math.max(coord.lastSeen, prior?.lastActiveAt ?? 0),
126
+ escalated: false, // alive again ⇒ re-arm the escalation
127
+ ...(coord.label ? { label: coord.label } : {}),
128
+ });
129
+ return;
130
+ }
131
+
132
+ // Coordinator is NOT alive (offline, heartbeat-stale, or vanished). Only escalate a coordinator we
133
+ // actually observed alive — never fire on cold/unknown startup state (mirrors the member watch).
134
+ const watch = teamCoordinatorWatchTracker.get(teamId);
135
+ if (!watch || watch.coordinatorId !== coordinatorId || watch.escalated) return;
136
+ watch.escalated = true; // one escalation per gone-spell, regardless of outcome below
137
+ out.goneCoordinatorIds.push(coordinatorId);
138
+
139
+ // A terminal marker means a #645 clean terminus (graceful shutdown) or a #633 crash the operator was
140
+ // already surfaced on — not a SILENT death. Suppress.
141
+ if (getCrashReport(coordinatorId) || hasTerminalEvent(coordinatorId)) {
142
+ out.suppressedTeamIds.push(teamId);
143
+ return;
144
+ }
145
+
146
+ // Escalate up the lineage, falling back to the configured human/steward target (#639/#642 pattern).
147
+ const escalationTarget = resolveCoordinatorAgentId(coordinatorId) ?? stewardFallbackTarget();
148
+ const label = watch.label ?? coordinatorId;
149
+ const sent = escalationTarget
150
+ ? notifyTarget(
151
+ "team.coordinator_gone",
152
+ { teamId, agentId: coordinatorId, parent: resolveCoordinatorAgentId(coordinatorId) ?? undefined },
153
+ escalationTarget,
154
+ "Team coordinator went silent",
155
+ `Coordinator \`${label}\` of team \`${teamId}\` has gone silent with no terminal event — nobody is driving the team and its members are no longer being listened to. Take over coordination, restart the team, or stand it down.`,
156
+ { kind: "team.coordinator_gone", teamId, coordinatorId, escalationTarget, label: watch.label },
157
+ )
158
+ : null;
159
+ if (sent) out.escalatedTeamIds.push(teamId);
160
+
161
+ // No-silent-dissolve: surface the coordinator-gone team on the dashboard even when there is no
162
+ // escalation target configured — it must never be silently abandoned.
163
+ createActivityEvent({
164
+ clientId: `team-coordinator-gone-${teamId}-${coordinatorId}-${now}`,
165
+ kind: "state",
166
+ title: "Team coordinator went silent",
167
+ body: sent
168
+ ? `Coordinator ${label} of team ${teamId} went silent — escalated to ${sent}.`
169
+ : `Coordinator ${label} of team ${teamId} went silent — no escalation target configured (set AGENT_RELAY_WORKSPACE_STEWARD_FALLBACK).`,
170
+ meta: teamId,
171
+ icon: "ti-user-x",
172
+ view: "teams",
173
+ agentId: coordinatorId,
174
+ metadata: {
175
+ source: "server",
176
+ maintenanceJobId: "team-idle-watchdog",
177
+ teamId,
178
+ coordinatorId,
179
+ escalationTarget: sent ?? null,
180
+ },
181
+ });
182
+ }
183
+
87
184
  function teamIdlePayload(teamId: string, coordinatorId: string, thresholdMinutes: number, idleForMs: number, firstNoBusyAt: number) {
88
185
  const roster = getTeamRoster(teamId).map((member) => {
89
186
  const providerState = agentProviderState(member);
@@ -145,12 +242,17 @@ export function runTeamIdleWatchdog(input: { now?: number } = {}): Record<string
145
242
  const alreadyPingedTeamIds: string[] = [];
146
243
  const missingCoordinatorTeamIds: string[] = [];
147
244
  const memberSilence = { pingedMembers: [] as string[], watchedMembers: 0 };
245
+ const coordinatorGone = { escalatedTeamIds: [] as string[], suppressedTeamIds: [] as string[], goneCoordinatorIds: [] as string[] };
148
246
 
149
247
  for (const team of listActiveTeams()) {
150
248
  seen.add(team.id);
151
249
  // #634 — runs every cycle, independent of the team-level idle gate below: a single silent
152
250
  // member must surface even while OTHER members keep the team "busy".
153
251
  detectSilentMembers(team.id, team.coordinatorId, now, thresholdMs, memberSilence);
252
+ // #412/#468 — coordinator-gone escalation, also every cycle and independent of the idle gate: a
253
+ // dead coordinator must surface even if the team isn't (yet) idle, and the idle-ping path can't
254
+ // reach it (it would just ping the void).
255
+ detectGoneCoordinator(team.id, team.coordinatorId, now, thresholdMs, coordinatorGone);
154
256
  const busy = getTeamBusyState(team.id, cutoff);
155
257
  if (busy.busyMemberIds.length > 0) {
156
258
  teamIdleTracker.delete(team.id);
@@ -182,6 +284,7 @@ export function runTeamIdleWatchdog(input: { now?: number } = {}): Record<string
182
284
 
183
285
  for (const key of teamIdleTracker.keys()) if (!seen.has(key)) teamIdleTracker.delete(key);
184
286
  for (const key of teamMemberWatchTracker.keys()) if (!seen.has(key)) teamMemberWatchTracker.delete(key);
287
+ for (const key of teamCoordinatorWatchTracker.keys()) if (!seen.has(key)) teamCoordinatorWatchTracker.delete(key);
185
288
 
186
289
  return {
187
290
  enabled: true,
@@ -194,6 +297,9 @@ export function runTeamIdleWatchdog(input: { now?: number } = {}): Record<string
194
297
  missingCoordinatorTeamIds,
195
298
  silentMemberIds: memberSilence.pingedMembers,
196
299
  watchedMembers: memberSilence.watchedMembers,
300
+ coordinatorGoneTeamIds: coordinatorGone.escalatedTeamIds,
301
+ coordinatorGoneSuppressedTeamIds: coordinatorGone.suppressedTeamIds,
302
+ goneCoordinatorIds: coordinatorGone.goneCoordinatorIds,
197
303
  };
198
304
  }
199
305
 
@@ -5,6 +5,7 @@ export type NotificationType =
5
5
  | "agent.exited"
6
6
  | "agent.spawn_failed"
7
7
  | "agent.context_threshold"
8
+ | "agent.stuck_busy"
8
9
  | "agent.quota_threshold"
9
10
  | "agent.resume_budget_warning"
10
11
  | "agent.resume_budget_exhausted"
@@ -12,6 +13,7 @@ export type NotificationType =
12
13
  | "agent.rate_limit_resume"
13
14
  | "team.idle"
14
15
  | "team.member_silent"
16
+ | "team.coordinator_gone"
15
17
  | "plan.bound_agent_exited"
16
18
  | "workspace.steward_task"
17
19
  | "workspace.steward_handoff"
@@ -58,6 +60,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
58
60
  "agent.exited": null,
59
61
  "agent.spawn_failed": null,
60
62
  "agent.context_threshold": 15 * MINUTES,
63
+ "agent.stuck_busy": null,
61
64
  "agent.quota_threshold": 15 * MINUTES,
62
65
  "agent.resume_budget_warning": null,
63
66
  "agent.resume_budget_exhausted": null,
@@ -65,6 +68,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
65
68
  "agent.rate_limit_resume": 15 * MINUTES,
66
69
  "team.idle": null,
67
70
  "team.member_silent": null,
71
+ "team.coordinator_gone": null,
68
72
  "plan.bound_agent_exited": null,
69
73
  "workspace.steward_task": null,
70
74
  "workspace.steward_handoff": null,
@@ -26,3 +26,22 @@ export interface TeamMemberWatch {
26
26
 
27
27
  // teamId → (memberId → watch)
28
28
  export const teamMemberWatchTracker = new Map<string, Map<string, TeamMemberWatch>>();
29
+
30
+ // #412/#468 — per-team COORDINATOR liveness watch. The member watch above deliberately skips the
31
+ // coordinator (it's the ping recipient, not a watched worker), so a coordinator that dies silently
32
+ // is invisible to it. This tracker remembers the coordinator's last-alive observation so a later
33
+ // silence escalates ONCE to the human/steward fallback instead of dead-ending — a coordinator-gone
34
+ // team must never be silently abandoned.
35
+ export interface TeamCoordinatorWatch {
36
+ /** Identity of the coordinator this watch is for; a re-elected/replaced coordinator resets it. */
37
+ coordinatorId: string;
38
+ /** Most recent time the coordinator was seen alive (non-offline, fresh heartbeat). */
39
+ lastActiveAt: number;
40
+ /** Whether the coordinator's CURRENT gone-spell already escalated (re-armed when it recovers). */
41
+ escalated: boolean;
42
+ /** Coordinator label for a readable escalation. */
43
+ label?: string;
44
+ }
45
+
46
+ // teamId → coordinator watch
47
+ export const teamCoordinatorWatchTracker = new Map<string, TeamCoordinatorWatch>();