agent-relay-server 0.84.0 → 0.85.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 (79) hide show
  1. package/docs/openapi.json +39 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-BTBQr8uG.js → MessageBubble-DqFyach0.js} +2 -2
  4. package/public/assets/{MessageBubble-BTBQr8uG.js.map → MessageBubble-DqFyach0.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-CoWJJVyD.js → PlanGraphPanel-CCvKbYWe.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CoWJJVyD.js.map → PlanGraphPanel-CCvKbYWe.js.map} +1 -1
  7. package/public/assets/{activity-XqRFu_9T.js → activity-C6RvknQQ.js} +2 -2
  8. package/public/assets/{activity-XqRFu_9T.js.map → activity-C6RvknQQ.js.map} +1 -1
  9. package/public/assets/{agents-BWmmWj4Q.js → agents-D7HZJ5eG.js} +2 -2
  10. package/public/assets/{agents-BWmmWj4Q.js.map → agents-D7HZJ5eG.js.map} +1 -1
  11. package/public/assets/{automation-DRtOENs0.js → automation-BUazA3lr.js} +2 -2
  12. package/public/assets/{automation-DRtOENs0.js.map → automation-BUazA3lr.js.map} +1 -1
  13. package/public/assets/{chat-gfUxbTus.js → chat-DBTEaf-H.js} +2 -2
  14. package/public/assets/{chat-gfUxbTus.js.map → chat-DBTEaf-H.js.map} +1 -1
  15. package/public/assets/{formatted-body-impl-DpNHGXXQ.js → formatted-body-impl-KG0iExZ-.js} +2 -2
  16. package/public/assets/{formatted-body-impl-DpNHGXXQ.js.map → formatted-body-impl-KG0iExZ-.js.map} +1 -1
  17. package/public/assets/index-LUIvWcx6.css +2 -0
  18. package/public/assets/index-TZskQfQL.js +23 -0
  19. package/public/assets/index-TZskQfQL.js.map +1 -0
  20. package/public/assets/{insights-DPaJ5Hme.js → insights-JL1s7Uul.js} +2 -2
  21. package/public/assets/{insights-DPaJ5Hme.js.map → insights-JL1s7Uul.js.map} +1 -1
  22. package/public/assets/{maintenance-DsszLb92.js → maintenance-CTMfhXFM.js} +2 -2
  23. package/public/assets/{maintenance-DsszLb92.js.map → maintenance-CTMfhXFM.js.map} +1 -1
  24. package/public/assets/{managed-agents-DqWPxFE0.js → managed-agents-C4cb3xQ4.js} +2 -2
  25. package/public/assets/{managed-agents-DqWPxFE0.js.map → managed-agents-C4cb3xQ4.js.map} +1 -1
  26. package/public/assets/{markdown-preview-impl-CWR4ILHK.js → markdown-preview-impl-DQIi0O7r.js} +2 -2
  27. package/public/assets/{markdown-preview-impl-CWR4ILHK.js.map → markdown-preview-impl-DQIi0O7r.js.map} +1 -1
  28. package/public/assets/{memory-C2s9bJ38.js → memory-DJPXFdgw.js} +2 -2
  29. package/public/assets/{memory-C2s9bJ38.js.map → memory-DJPXFdgw.js.map} +1 -1
  30. package/public/assets/{messages-DHRJNAvy.js → messages-RSD2g4Wx.js} +2 -2
  31. package/public/assets/{messages-DHRJNAvy.js.map → messages-RSD2g4Wx.js.map} +1 -1
  32. package/public/assets/{orchestrators-BGNNllJc.js → orchestrators-BGIkULBF.js} +2 -2
  33. package/public/assets/{orchestrators-BGNNllJc.js.map → orchestrators-BGIkULBF.js.map} +1 -1
  34. package/public/assets/{overview-C1vZfyhW.js → overview-ByE9V8pe.js} +2 -2
  35. package/public/assets/{overview-C1vZfyhW.js.map → overview-ByE9V8pe.js.map} +1 -1
  36. package/public/assets/{pairs-C1uzo8x0.js → pairs-C284ntvD.js} +2 -2
  37. package/public/assets/{pairs-C1uzo8x0.js.map → pairs-C284ntvD.js.map} +1 -1
  38. package/public/assets/{security-XhAAP6q3.js → security-vDglScsg.js} +2 -2
  39. package/public/assets/{security-XhAAP6q3.js.map → security-vDglScsg.js.map} +1 -1
  40. package/public/assets/{select-DEGUFEjx.js → select-DaOiWDBR.js} +2 -2
  41. package/public/assets/{select-DEGUFEjx.js.map → select-DaOiWDBR.js.map} +1 -1
  42. package/public/assets/{settings-DTbVUdlZ.js → settings-DiLcbSqx.js} +2 -2
  43. package/public/assets/{settings-DTbVUdlZ.js.map → settings-DiLcbSqx.js.map} +1 -1
  44. package/public/assets/{tasks-zwGo7hon.js → tasks-DWJdJra3.js} +2 -2
  45. package/public/assets/{tasks-zwGo7hon.js.map → tasks-DWJdJra3.js.map} +1 -1
  46. package/public/assets/{teams-Brc5D8ox.js → teams-FejXkix4.js} +2 -2
  47. package/public/assets/{teams-Brc5D8ox.js.map → teams-FejXkix4.js.map} +1 -1
  48. package/public/assets/{terminal-viewer-impl-C6yUAL8G.js → terminal-viewer-impl-Bkt1w42o.js} +2 -2
  49. package/public/assets/{terminal-viewer-impl-C6yUAL8G.js.map → terminal-viewer-impl-Bkt1w42o.js.map} +1 -1
  50. package/public/assets/{user-avatar-Bw1R011j.js → user-avatar-ClE2dMD8.js} +2 -2
  51. package/public/assets/{user-avatar-Bw1R011j.js.map → user-avatar-ClE2dMD8.js.map} +1 -1
  52. package/public/assets/{work-queue-ge4R7keo.js → work-queue-DZdv9kOb.js} +2 -2
  53. package/public/assets/{work-queue-ge4R7keo.js.map → work-queue-DZdv9kOb.js.map} +1 -1
  54. package/public/index.html +2 -2
  55. package/src/agent-death-diagnosis.ts +255 -0
  56. package/src/agent-lifecycle-events.ts +42 -19
  57. package/src/bus.ts +18 -4
  58. package/src/db/agents.ts +35 -2
  59. package/src/db/crash-reports.ts +115 -0
  60. package/src/db/index.ts +1 -0
  61. package/src/db/migrations.ts +5 -0
  62. package/src/maintenance/jobs.ts +6 -1
  63. package/src/maintenance/teams.ts +68 -1
  64. package/src/mcp/index.ts +2 -1
  65. package/src/mcp/tools-messaging.ts +38 -0
  66. package/src/notification-types.ts +2 -0
  67. package/src/routes/agents.ts +11 -1
  68. package/src/routes/commands.ts +21 -3
  69. package/src/routes/index.ts +2 -1
  70. package/src/routes/orchestrator.ts +14 -1
  71. package/src/services/terminal-provider-exit.ts +33 -0
  72. package/src/spawn-targets.ts +19 -8
  73. package/src/team-idle-tracker.ts +15 -0
  74. package/src/workspace-actions.ts +5 -1
  75. package/src/workspace-auto-merge.ts +87 -4
  76. package/src/workspace-merge.ts +10 -0
  77. package/public/assets/index-D8TVUdlC.js +0 -22
  78. package/public/assets/index-D8TVUdlC.js.map +0 -1
  79. package/public/assets/index-DgZLDL0K.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-D8TVUdlC.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-Bw1R011j.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-TZskQfQL.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-ClE2dMD8.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar-Bw1R011j.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-ClE2dMD8.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-DYaQAXRv.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-D8TVUdlC.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-ge4R7keo.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-DYaQAXRv.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-TZskQfQL.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-DZdv9kOb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-ge4R7keo.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-DZdv9kOb.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-D8TVUdlC.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-TZskQfQL.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-DgZLDL0K.css">
40
+ <link rel="stylesheet" crossorigin href="./assets/index-LUIvWcx6.css">
41
41
  </head>
42
42
  <body class="bg-background text-foreground antialiased">
43
43
  <div id="root"></div>
@@ -0,0 +1,255 @@
1
+ // #636 — crash/exit post-mortem classifier. A self-contained, pure mapping from the evidence
2
+ // captured at an agent's death (orchestrator exit diagnostics + the server's own agent state)
3
+ // to a single likely-cause label + supporting tail. It does NO IO: every host-side probe (OOM
4
+ // cgroup/journal, tmux pane status, log tail) is gathered upstream (orchestrator
5
+ // `diagnoseSessionExit`, runner provider-exit diagnostics) and handed in here, so the verdict is
6
+ // deterministic and unit-testable per failure mode. The output rides the #633 terminal event and
7
+ // is persisted as a CrashReport so the WHY survives the reaper.
8
+
9
+ import { extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
10
+ import type { AgentCard, AgentDeathCause, AgentDeathDiagnosis, CrashReport, ManagedSessionExitDiagnostics } from "./types";
11
+
12
+ export interface DeathEvidence {
13
+ /** The offline cause the caller already has (e.g. "heartbeat lost", "shutdown", exit reason). */
14
+ reason: string;
15
+ /** Orchestrator/runner-captured exit diagnostics, when the death was observed as a session exit. */
16
+ exit?: ManagedSessionExitDiagnostics;
17
+ /** The dead agent's `meta` blob (lastError, lastExit, terminalProviderExit, terminalFailureReason, auth…). */
18
+ agentMeta?: Record<string, unknown>;
19
+ /** Pre-death agent status, for distinguishing a busy stall from an idle one. */
20
+ status?: string;
21
+ /** Server's last-seen heartbeat for the agent. */
22
+ lastHeartbeatAt?: number;
23
+ /** Last time the agent reported busy (turn activity). */
24
+ lastBusyAt?: number;
25
+ /** Active work titles at death. */
26
+ activeWork?: string[];
27
+ /** Evaluation clock (defaults to Date.now()). */
28
+ now?: number;
29
+ }
30
+
31
+ const SIGNAL_RE = /\bSIG(KILL|TERM|SEGV|ABRT|BUS|INT|HUP|QUIT)\b/i;
32
+ const OOM_RE = /\b(out of memory|oom[- ]?kill(?:ed|er)?|killed process \d+|cannot allocate memory|javascript heap out of memory)\b/i;
33
+ const TOKEN_401_RE = /\b(401|403\b.*token|unauthorized|invalid[_ ]token|token (?:expired|revoked|invalid)|authentication failed|please run\s+\/login|run \/login|oauth token)\b/i;
34
+ const ONBOARDING_RE = /(do you trust|trust this folder|onboarding|first[- ]run|select a theme|choose your theme|accept the terms|welcome to claude)/i;
35
+ const CRASH_RE = /\b(panic|segfault|segmentation fault|core dumped|traceback \(most recent|unhandled (?:exception|rejection)|fatal error|abort(?:ed)?\b|stack overflow)\b/i;
36
+ const TRANSPORT_RE = /\b(heartbeat|transport|websocket|connection (?:lost|closed|reset)|disconnect)\b/i;
37
+
38
+ function pushUnique(list: string[], value: string | undefined): void {
39
+ if (value && !list.includes(value)) list.push(value);
40
+ }
41
+
42
+ function record(meta: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
43
+ const value = meta?.[key];
44
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
45
+ }
46
+
47
+ function str(value: unknown): string | undefined {
48
+ return typeof value === "string" && value.length > 0 ? value : undefined;
49
+ }
50
+
51
+ function num(value: unknown): number | undefined {
52
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
53
+ }
54
+
55
+ /** Resolve a termination signal name + exit code from the orchestrator exit diagnostics or agent meta. */
56
+ function resolveSignalAndCode(ev: DeathEvidence): { signal?: string; exitCode?: number } {
57
+ const exit = ev.exit;
58
+ let signal = exit?.signal;
59
+ let exitCode = exit?.exitCode;
60
+ const systemd = exit?.systemd;
61
+ if (!signal && systemd?.execMainCode && /killed|dumped|signal/i.test(systemd.execMainCode)) {
62
+ // systemd ExecMainStatus carries the signal name/number when ExecMainCode is killed/dumped.
63
+ signal = systemd.execMainStatus ? `SIG${systemd.execMainStatus.replace(/^SIG/i, "")}` : undefined;
64
+ }
65
+ if (exitCode === undefined && systemd?.execMainCode === "exited" && systemd.execMainStatus) {
66
+ const n = Number(systemd.execMainStatus);
67
+ if (Number.isFinite(n)) exitCode = n;
68
+ }
69
+ const tpe = record(ev.agentMeta, "terminalProviderExit");
70
+ if (!signal) signal = str(tpe?.signal);
71
+ if (exitCode === undefined) exitCode = num(tpe?.exitCode);
72
+ return { signal, exitCode };
73
+ }
74
+
75
+ /** Build the haystack of free text scanned by the known-pattern probes. */
76
+ function haystack(ev: DeathEvidence): string {
77
+ const parts: string[] = [ev.reason];
78
+ if (ev.exit?.logTail) parts.push(...ev.exit.logTail);
79
+ pushUnique(parts, ev.exit?.lastError);
80
+ pushUnique(parts, str(ev.agentMeta?.lastError));
81
+ pushUnique(parts, str(ev.agentMeta?.terminalFailureReason));
82
+ pushUnique(parts, str(ev.agentMeta?.terminalFailureMessage));
83
+ const lastExit = record(ev.agentMeta, "lastExit");
84
+ pushUnique(parts, str(lastExit?.lastError));
85
+ const tpe = record(ev.agentMeta, "terminalProviderExit");
86
+ pushUnique(parts, str(tpe?.reason));
87
+ return parts.join("\n");
88
+ }
89
+
90
+ function logTailOf(ev: DeathEvidence): string[] | undefined {
91
+ if (ev.exit?.logTail?.length) return ev.exit.logTail;
92
+ const lastExit = record(ev.agentMeta, "lastExit");
93
+ const tail = lastExit?.logTail;
94
+ if (Array.isArray(tail) && tail.length) return tail.filter((x): x is string => typeof x === "string");
95
+ return undefined;
96
+ }
97
+
98
+ /**
99
+ * Classify an agent death into a single likely-cause label with supporting evidence (#636).
100
+ * Pure — all host probes are pre-gathered into {@link DeathEvidence}. Probe order is by
101
+ * conclusiveness: a clean exit code never masks a token-401 / onboarding gate that also exits 0.
102
+ */
103
+ export function classifyAgentDeath(ev: DeathEvidence): AgentDeathDiagnosis {
104
+ const now = ev.now ?? Date.now();
105
+ const text = haystack(ev);
106
+ const logTail = logTailOf(ev);
107
+ const { signal, exitCode } = resolveSignalAndCode(ev);
108
+ const evidence: string[] = [];
109
+
110
+ const systemdResult = ev.exit?.systemd?.result;
111
+ const resumeId = str(record(ev.agentMeta, "terminalProviderExit")?.claudeResumeId);
112
+ const heartbeatGapMs = ev.lastHeartbeatAt !== undefined ? Math.max(0, now - ev.lastHeartbeatAt) : undefined;
113
+
114
+ const base: Pick<AgentDeathDiagnosis, "signal" | "exitCode" | "logTail" | "lastHeartbeatAt" | "heartbeatGapMs" | "lastBusyAt" | "activeWork" | "detectedAt"> = {
115
+ ...(signal ? { signal } : {}),
116
+ ...(exitCode !== undefined ? { exitCode } : {}),
117
+ ...(logTail ? { logTail } : {}),
118
+ ...(ev.lastHeartbeatAt !== undefined ? { lastHeartbeatAt: ev.lastHeartbeatAt } : {}),
119
+ ...(heartbeatGapMs !== undefined ? { heartbeatGapMs } : {}),
120
+ ...(ev.lastBusyAt !== undefined ? { lastBusyAt: ev.lastBusyAt } : {}),
121
+ ...(ev.activeWork?.length ? { activeWork: ev.activeWork } : {}),
122
+ detectedAt: now,
123
+ };
124
+
125
+ const verdict = (cause: AgentDeathCause, confidence: AgentDeathDiagnosis["confidence"], summary: string, oom?: boolean): AgentDeathDiagnosis => ({
126
+ cause,
127
+ confidence,
128
+ summary,
129
+ evidence,
130
+ ...(oom !== undefined ? { oom } : {}),
131
+ ...base,
132
+ });
133
+
134
+ // 1) OOM — most conclusive: positive cgroup/journal/systemd signal, then log text.
135
+ if (ev.exit?.oom) {
136
+ pushUnique(evidence, `OOM detected via ${ev.exit.oom.source}${ev.exit.oom.detail ? `: ${ev.exit.oom.detail}` : ""}`);
137
+ return verdict("oom", "high", "Out-of-memory kill", true);
138
+ }
139
+ if (systemdResult === "oom-kill") {
140
+ pushUnique(evidence, "systemd Result=oom-kill");
141
+ return verdict("oom", "high", "Out-of-memory kill (systemd Result=oom-kill)", true);
142
+ }
143
+ if (OOM_RE.test(text)) {
144
+ pushUnique(evidence, "log/error text matched an out-of-memory pattern");
145
+ return verdict("oom", "medium", "Likely out-of-memory (log pattern)", true);
146
+ }
147
+
148
+ // 2) token-401 — auth expiry/revocation (cf. managed-token-revocation). Can exit 0, so before clean-exit.
149
+ if (TOKEN_401_RE.test(text)) {
150
+ pushUnique(evidence, "log/error text matched a token-401 / auth-failure pattern");
151
+ return verdict("token-401", "high", "Token expired/revoked (401 auth failure)");
152
+ }
153
+
154
+ // 3) onboarding-gate — first-run/trust gate crash-loop (cf. isolated-claude onboarding gate).
155
+ if (ONBOARDING_RE.test(text)) {
156
+ const fast = (ev.exit?.runtimeMs ?? Infinity) < 60_000;
157
+ if (fast) pushUnique(evidence, `died fast (${Math.round((ev.exit?.runtimeMs ?? 0) / 1000)}s) at a first-run/onboarding gate`);
158
+ else pushUnique(evidence, "log/error text matched an onboarding/trust-gate pattern");
159
+ return verdict("onboarding-gate", fast ? "high" : "medium", "Stuck at a first-run onboarding/trust gate");
160
+ }
161
+
162
+ // 4) provider-crash — CLI panic/segfault/model-unavailable, or a crash signal.
163
+ const modelUnavailable = extractClaudeModelUnavailableMessage(text);
164
+ if (modelUnavailable) {
165
+ pushUnique(evidence, modelUnavailable);
166
+ return verdict("provider-crash", "high", "Provider unavailable (model/CLI error)");
167
+ }
168
+ if (signal && /SEGV|ABRT|BUS/i.test(signal)) {
169
+ pushUnique(evidence, `provider terminated by ${signal}`);
170
+ return verdict("provider-crash", "high", `Provider crashed (${signal})`);
171
+ }
172
+ if (CRASH_RE.test(text)) {
173
+ pushUnique(evidence, "log/error text matched a crash pattern (panic/segfault/traceback)");
174
+ return verdict("provider-crash", "medium", "Likely provider crash (log pattern)");
175
+ }
176
+
177
+ // 5) killed — external SIGKILL/SIGTERM that isn't OOM and isn't a parent-initiated shutdown.
178
+ if (signal && /KILL|TERM|INT|HUP|QUIT/i.test(signal)) {
179
+ pushUnique(evidence, `process terminated by ${signal}`);
180
+ return verdict("killed", "high", `Process killed (${signal})`);
181
+ }
182
+
183
+ // 6) clean-exit — exit 0 / systemd success / Claude native auto-compaction resume handoff.
184
+ if (resumeId) {
185
+ pushUnique(evidence, `clean self-exit with a Claude resume id (${resumeId}) — native context auto-compaction`);
186
+ return verdict("clean-exit", "high", "Clean self-exit (Claude auto-compaction / resume handoff)");
187
+ }
188
+ if (exitCode === 0 || systemdResult === "success") {
189
+ pushUnique(evidence, exitCode === 0 ? "process exited 0" : "systemd Result=success");
190
+ return verdict("clean-exit", "high", "Clean exit (exit code 0)");
191
+ }
192
+ if (exitCode !== undefined && exitCode > 0) {
193
+ pushUnique(evidence, `process exited non-zero (code ${exitCode})`);
194
+ return verdict("provider-crash", "medium", `Non-zero exit (code ${exitCode})`);
195
+ }
196
+
197
+ // 7) transport-loss — heartbeat/bus gap with no exit diagnostics: the process may even be alive,
198
+ // but the relay lost the agent. This is the silent-stall backstop (#634).
199
+ if (TRANSPORT_RE.test(ev.reason) && !ev.exit) {
200
+ pushUnique(evidence, `lost contact: ${ev.reason}${heartbeatGapMs !== undefined ? ` (heartbeat gap ${Math.round(heartbeatGapMs / 1000)}s)` : ""}`);
201
+ return verdict("transport-loss", "medium", "Lost heartbeat/transport (no exit captured)");
202
+ }
203
+
204
+ pushUnique(evidence, ev.reason || "no conclusive signal");
205
+ return verdict("unknown", "low", "Cause undetermined");
206
+ }
207
+
208
+ function activeWorkTitles(meta: Record<string, unknown> | undefined): string[] | undefined {
209
+ const work = meta?.activeWork;
210
+ if (!Array.isArray(work) || work.length === 0) return undefined;
211
+ const titles = work
212
+ .map((item) => (item && typeof item === "object" ? str((item as Record<string, unknown>).title) ?? str((item as Record<string, unknown>).kind) : undefined))
213
+ .filter((x): x is string => Boolean(x));
214
+ return titles.length ? titles : undefined;
215
+ }
216
+
217
+ /**
218
+ * Assemble {@link DeathEvidence} from a live agent card + the offline reason (and any orchestrator
219
+ * exit diagnostics), then classify. The single seam every death-detection site calls so evidence
220
+ * extraction (heartbeat, active work, lastExit) stays DRY.
221
+ */
222
+ export function diagnoseAgentDeath(input: {
223
+ agent?: AgentCard | null;
224
+ reason: string;
225
+ exit?: ManagedSessionExitDiagnostics;
226
+ now?: number;
227
+ }): AgentDeathDiagnosis {
228
+ const meta = input.agent?.meta;
229
+ return classifyAgentDeath({
230
+ reason: input.reason,
231
+ exit: input.exit,
232
+ agentMeta: meta,
233
+ status: input.agent?.status,
234
+ lastHeartbeatAt: input.agent?.lastSeen,
235
+ activeWork: activeWorkTitles(meta),
236
+ now: input.now,
237
+ });
238
+ }
239
+
240
+ /** Build a persistable {@link CrashReport} from a dead agent's card + its diagnosis. */
241
+ export function crashReportFor(agent: AgentCard, reason: string, diagnosis: AgentDeathDiagnosis): CrashReport {
242
+ const provider = str(agent.meta?.provider) ?? agent.providerCapabilities?.model?.provider;
243
+ const spawnRequestId = str(agent.meta?.spawnRequestId);
244
+ return {
245
+ agentId: agent.id,
246
+ ...(agent.spawnedBy ? { parentId: agent.spawnedBy } : {}),
247
+ ...(agent.teamId ? { teamId: agent.teamId } : {}),
248
+ ...(spawnRequestId ? { spawnRequestId } : {}),
249
+ ...(provider ? { provider } : {}),
250
+ ...(agent.label ? { label: agent.label } : {}),
251
+ reason,
252
+ diagnosis,
253
+ createdAt: diagnosis.detectedAt,
254
+ };
255
+ }
@@ -1,8 +1,14 @@
1
1
  import { emitRelayEvent } from "./events";
2
2
  import { getNotificationsConfig } from "./config-store";
3
3
  import { routeNotification } from "./notification-router";
4
- import { getAgent } from "./db";
5
- import type { AgentCard } from "./types";
4
+ import { getAgent, markAgentReadyNotified, recordCrashReport } from "./db";
5
+ import { crashReportFor } from "./agent-death-diagnosis";
6
+ import type { AgentCard, AgentDeathDiagnosis } from "./types";
7
+
8
+ /** One-line cause tag appended to a death notification body, e.g. " · why: Clean exit (exit code 0)". */
9
+ function diagnosisSuffix(diagnosis?: AgentDeathDiagnosis): string {
10
+ return diagnosis ? ` · why: ${diagnosis.summary}` : "";
11
+ }
6
12
 
7
13
  // #308/#239 — async spawn lifecycle signals. A spawn-capable agent fires a spawn, gets its
8
14
  // spawnRequestId back immediately, and is WOKEN on ready/exit/failure instead of burning its
@@ -11,15 +17,22 @@ import type { AgentCard } from "./types";
11
17
 
12
18
  // Spawned children that have fired `agent.ready` during this relay lifetime. Serves two jobs:
13
19
  // (1) dedup the ready push — it must fire exactly once per child, not on every heartbeat
14
- // re-registration; and (2) classify a later offline transition: a child IN this set exited
15
- // cleanly (`agent.exited`); one that never entered it went offline without ever becoming ready,
20
+ // re-registration; and (2) classify a later offline transition: a child that HAD become ready
21
+ // exited cleanly (`agent.exited`); one that never did went offline without ever becoming ready,
16
22
  // i.e. a spawn that failed to come up (`agent.spawn_failed` — the "process started ≠ ready"
17
- // crash-loop / onboarding-gate mode the isolated-Claude failure proves). In-memory by design: a
18
- // child still alive across a relay restart was ready long ago and its parent already knows, so a
19
- // post-restart exit reads as the benign `agent.exited` default — a restart must never manufacture
20
- // a spurious failure alert.
23
+ // crash-loop / onboarding-gate mode the isolated-Claude failure proves).
24
+ //
25
+ // This set is in-memory, so a relay restart wipes it. A restart must never manufacture a spurious
26
+ // failure alert for a child that was ready long ago — so readiness is ALSO persisted as a durable
27
+ // `readyNotifiedAt` meta stamp (markAgentReadyNotified). Classification consults the set first and
28
+ // falls back to the persisted stamp, so a post-restart clean exit still reads as `agent.exited`.
21
29
  const readyFired = new Set<string>();
22
30
 
31
+ /** A child counts as "had become ready" if the live set saw it OR the durable stamp survives a restart. */
32
+ function everReady(child: AgentCard): boolean {
33
+ return typeof child.meta?.readyNotifiedAt === "number";
34
+ }
35
+
23
36
  function describe(child: AgentCard): string {
24
37
  return child.label ? `${child.label} (\`${child.id}\`)` : `\`${child.id}\``;
25
38
  }
@@ -46,6 +59,9 @@ export function notifyAgentReady(childId: string): void {
46
59
  if (!child || !parent) return;
47
60
  if (readyFired.has(childId)) return;
48
61
  readyFired.add(childId);
62
+ // Persist a durable readiness stamp so a post-restart offline edge still classifies as exited
63
+ // (the in-memory set is wiped on restart). Idempotent — only stamped on first ready.
64
+ markAgentReadyNotified(childId);
49
65
 
50
66
  emitRelayEvent({
51
67
  type: "agent.ready",
@@ -70,24 +86,31 @@ export function notifyAgentReady(childId: string): void {
70
86
  }
71
87
 
72
88
  /**
73
- * A spawned child went offline (heartbeat-reaped, or shut down). Classifies via `readyFired`: a
74
- * child that had become ready exited (`agent.exited`); one that never did failed to come up
75
- * (`agent.spawn_failed`). Routes to the `spawned_by` parent. No-op for non-spawned agents.
89
+ * A spawned child went offline (heartbeat-reaped, or shut down). Classifies via readiness (live set
90
+ * or the durable `readyNotifiedAt` stamp): a child that had become ready exited (`agent.exited`);
91
+ * one that never did failed to come up (`agent.spawn_failed`). Routes to the `spawned_by` parent.
92
+ * No-op for non-spawned agents.
76
93
  * `reason` is the offline cause (e.g. "heartbeat lost", "shutdown").
77
94
  */
78
- export function notifyAgentOffline(childId: string, reason: string): void {
95
+ export function notifyAgentOffline(childId: string, reason: string, diagnosis?: AgentDeathDiagnosis): void {
79
96
  const child = getAgent(childId);
80
97
  const parent = child?.spawnedBy;
81
98
  if (!child || !parent) return;
82
- const wasReady = readyFired.delete(childId);
99
+ // Classify from the live set OR the durable readiness stamp. The `delete` always runs (cleanup of
100
+ // the in-memory set); the persisted stamp covers a child that became ready before a relay restart.
101
+ const wasReady = readyFired.delete(childId) || everReady(child);
83
102
  const config = getNotificationsConfig();
84
103
 
104
+ // #636 — persist the post-mortem keyed by agentId so the WHY survives the reaper, regardless of
105
+ // whether the parent push is enabled. Recorded once per terminal-event emission.
106
+ if (diagnosis) recordCrashReport(crashReportFor(child, reason, diagnosis));
107
+
85
108
  if (wasReady) {
86
109
  emitRelayEvent({
87
110
  type: "agent.exited",
88
111
  source: "server",
89
112
  subject: childId,
90
- data: { agentId: childId, parent, reason, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child) },
113
+ data: { agentId: childId, parent, reason, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child), ...(diagnosis ? { diagnosis } : {}) },
91
114
  });
92
115
  if (!config.enabled || !config.agentExited) return;
93
116
  routeNotification({
@@ -96,8 +119,8 @@ export function notifyAgentOffline(childId: string, reason: string): void {
96
119
  recipients: [parent],
97
120
  message: {
98
121
  subject: "Spawned agent exited",
99
- body: `⏹ Your spawned agent ${describe(child)} exited — ${reason}.`,
100
- payload: { kind: "agent.exited", agentId: childId, parent, reason },
122
+ body: `⏹ Your spawned agent ${describe(child)} exited — ${reason}${diagnosisSuffix(diagnosis)}.`,
123
+ payload: { kind: "agent.exited", agentId: childId, parent, reason, ...(diagnosis ? { diagnosis } : {}) },
101
124
  replyExpected: false,
102
125
  },
103
126
  });
@@ -108,7 +131,7 @@ export function notifyAgentOffline(childId: string, reason: string): void {
108
131
  type: "agent.spawn_failed",
109
132
  source: "server",
110
133
  subject: childId,
111
- data: { agentId: childId, parent, reason, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child), neverReady: true },
134
+ data: { agentId: childId, parent, reason, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child), neverReady: true, ...(diagnosis ? { diagnosis } : {}) },
112
135
  });
113
136
  if (!config.enabled || !config.agentSpawnFailed) return;
114
137
  routeNotification({
@@ -117,8 +140,8 @@ export function notifyAgentOffline(childId: string, reason: string): void {
117
140
  recipients: [parent],
118
141
  message: {
119
142
  subject: "Spawn failed",
120
- body: `❌ Your spawned agent ${describe(child)} went offline before ever becoming ready — ${reason}. Likely a crash-loop, onboarding gate, or bad launch; call relay_spawn_targets to re-check provider availability before retrying.`,
121
- payload: { kind: "agent.spawn_failed", agentId: childId, parent, reason },
143
+ body: `❌ Your spawned agent ${describe(child)} went offline before ever becoming ready — ${reason}${diagnosisSuffix(diagnosis)}. Likely a crash-loop, onboarding gate, or bad launch; call relay_spawn_targets to re-check provider availability before retrying.`,
144
+ payload: { kind: "agent.spawn_failed", agentId: childId, parent, reason, ...(diagnosis ? { diagnosis } : {}) },
122
145
  replyExpected: false,
123
146
  },
124
147
  });
package/src/bus.ts CHANGED
@@ -7,6 +7,7 @@ import { getCommand, updateCommand } from "./commands-db";
7
7
  import { emitCommandEvent } from "./command-events";
8
8
  import { getLifecycleManager } from "./lifecycle-manager";
9
9
  import { noteAgentTimelineEvent, noteCompactionCommandCompleted } from "./compaction-watch";
10
+ import { reconcileTerminalProviderExit } from "./services/terminal-provider-exit";
10
11
  import { registerAgent } from "./services/register-agent";
11
12
  import { authContextFromBus } from "./services/auth-context";
12
13
  import { commandAuthorizationResource, dispatchCommand } from "./services/dispatch-command";
@@ -143,10 +144,19 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
143
144
  case "heartbeat":
144
145
  withRegistered(ws, frame, (conn) => {
145
146
  if (conn.agentId) {
146
- const runtime = runtimeFromMeta(frame.payload.meta);
147
- heartbeat(conn.agentId, { instanceId: conn.instanceId, epoch: conn.epoch }, runtime);
148
- if (frame.payload.status) setStatus(conn.agentId, frame.payload.status, { instanceId: conn.instanceId, epoch: conn.epoch });
149
- emitAgentStatusEvent(conn.agentId);
147
+ // #633 a runner whose provider terminally exited stays alive and keeps sending idle
148
+ // keepalives (semanticStatus is never "offline"). Those must NOT resurrect an agent the
149
+ // server already marked offline for that terminal exit, or the death would keep masking
150
+ // itself. Ignore the keepalive (the row ages out via the normal offline-prune); a genuine
151
+ // resume sends a `status`/`register` frame, which clears the hold below.
152
+ const current = getAgent(conn.agentId);
153
+ const heldOffline = current?.status === "offline" && isRecord(current.meta?.terminalProviderExit);
154
+ if (!heldOffline) {
155
+ const runtime = runtimeFromMeta(frame.payload.meta);
156
+ heartbeat(conn.agentId, { instanceId: conn.instanceId, epoch: conn.epoch }, runtime);
157
+ if (frame.payload.status) setStatus(conn.agentId, frame.payload.status, { instanceId: conn.instanceId, epoch: conn.epoch });
158
+ emitAgentStatusEvent(conn.agentId);
159
+ }
150
160
  }
151
161
  if (frame.id) send(ws, { type: "ack", payload: { frameId: frame.id } });
152
162
  });
@@ -180,6 +190,10 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
180
190
  emitAgentStatusEvent(conn.agentId);
181
191
  // #237 stop-hook → instant ⚪↔🟡 changes detection on the turn-end edge.
182
192
  probeWorkspaceOnTurnEnd(before, after);
193
+ // #633 — silent clean-exit: a runner that stays alive after its provider terminally exited
194
+ // marks the offline edge with `terminalProviderExit`; the owning service turns that into a
195
+ // terminal event + #636 diagnosis (and clears the hold on a genuine resume).
196
+ reconcileTerminalProviderExit(conn.agentId, before, after);
183
197
  });
184
198
  return;
185
199
  case "ack":
package/src/db/agents.ts CHANGED
@@ -331,6 +331,36 @@ export function mergeAgentMeta(id: string, meta: Record<string, unknown>, guard?
331
331
  return result.changes > 0;
332
332
  }
333
333
 
334
+ /** #633/#634 — persist a durable "this child became ready" marker. Death classification
335
+ * (`notifyAgentOffline`) keys `agent.exited` vs `agent.spawn_failed` off readiness, but the live
336
+ * signal is an in-memory set that a relay restart wipes — so a long-ready child's later clean exit
337
+ * would mis-fire `spawn_failed`. This stamp survives the restart in SQLite, so the offline edge
338
+ * still classifies as `agent.exited`. Idempotent: only stamped once (first ready). */
339
+ export function markAgentReadyNotified(id: string, at: number = Date.now()): boolean {
340
+ const agent = getAgent(id);
341
+ if (!agent) return false;
342
+ if (typeof agent.meta?.readyNotifiedAt === "number") return false;
343
+ const merged = { ...(agent.meta ?? {}), readyNotifiedAt: at };
344
+ return getDb().query("UPDATE agents SET meta = ? WHERE id = ?").run(JSON.stringify(merged), id).changes > 0;
345
+ }
346
+
347
+ /** Delete specific keys from an agent's meta blob. Returns true if anything changed. Used to clear
348
+ * the #633 `terminalProviderExit` hold when a dead provider genuinely resumes. */
349
+ export function clearAgentMetaKeys(id: string, keys: string[]): boolean {
350
+ const agent = getAgent(id);
351
+ if (!agent?.meta) return false;
352
+ const meta = { ...agent.meta };
353
+ let changed = false;
354
+ for (const key of keys) {
355
+ if (key in meta) {
356
+ delete meta[key];
357
+ changed = true;
358
+ }
359
+ }
360
+ if (!changed) return false;
361
+ return getDb().query("UPDATE agents SET meta = ? WHERE id = ?").run(JSON.stringify(meta), id).changes > 0;
362
+ }
363
+
334
364
  export function recordAgentExitDiagnostics(id: string, diagnostics: ManagedSessionExitDiagnostics): AgentCard | null {
335
365
  const agent = getAgent(id);
336
366
  if (!agent) return null;
@@ -341,9 +371,12 @@ export function recordAgentExitDiagnostics(id: string, diagnostics: ManagedSessi
341
371
  lastExit: diagnostics,
342
372
  lastExitAt: diagnostics.detectedAt || now,
343
373
  };
374
+ // #636 — do NOT clobber last_seen with `now`: it is the agent's genuine last heartbeat, and the
375
+ // death diagnosis derives the real heartbeat gap from it (the orchestrator exit path diagnoses
376
+ // off the row returned here). "When we detected the exit" lives in meta.lastExitAt instead.
344
377
  const result = getDb()
345
- .query("UPDATE agents SET status = 'offline', ready = 0, meta = ?, last_seen = ? WHERE id = ?")
346
- .run(JSON.stringify(merged), now, id);
378
+ .query("UPDATE agents SET status = 'offline', ready = 0, meta = ? WHERE id = ?")
379
+ .run(JSON.stringify(merged), id);
347
380
  if (result.changes <= 0) return null;
348
381
  closeOpenPairsForAgent(id, now);
349
382
  return getAgent(id);