agent-relay-server 0.83.1 → 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.
- package/docs/openapi.json +151 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-BTBQr8uG.js → MessageBubble-DqFyach0.js} +2 -2
- package/public/assets/{MessageBubble-BTBQr8uG.js.map → MessageBubble-DqFyach0.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CoWJJVyD.js → PlanGraphPanel-CCvKbYWe.js} +2 -2
- package/public/assets/{PlanGraphPanel-CoWJJVyD.js.map → PlanGraphPanel-CCvKbYWe.js.map} +1 -1
- package/public/assets/{activity-XqRFu_9T.js → activity-C6RvknQQ.js} +2 -2
- package/public/assets/{activity-XqRFu_9T.js.map → activity-C6RvknQQ.js.map} +1 -1
- package/public/assets/{agents-BWmmWj4Q.js → agents-D7HZJ5eG.js} +2 -2
- package/public/assets/{agents-BWmmWj4Q.js.map → agents-D7HZJ5eG.js.map} +1 -1
- package/public/assets/{automation-DRtOENs0.js → automation-BUazA3lr.js} +2 -2
- package/public/assets/{automation-DRtOENs0.js.map → automation-BUazA3lr.js.map} +1 -1
- package/public/assets/{chat-gfUxbTus.js → chat-DBTEaf-H.js} +2 -2
- package/public/assets/{chat-gfUxbTus.js.map → chat-DBTEaf-H.js.map} +1 -1
- package/public/assets/display-CWMHll1J.js.map +1 -1
- package/public/assets/{formatted-body-impl-DpNHGXXQ.js → formatted-body-impl-KG0iExZ-.js} +2 -2
- package/public/assets/{formatted-body-impl-DpNHGXXQ.js.map → formatted-body-impl-KG0iExZ-.js.map} +1 -1
- package/public/assets/index-LUIvWcx6.css +2 -0
- package/public/assets/index-TZskQfQL.js +23 -0
- package/public/assets/index-TZskQfQL.js.map +1 -0
- package/public/assets/{insights-DPaJ5Hme.js → insights-JL1s7Uul.js} +2 -2
- package/public/assets/{insights-DPaJ5Hme.js.map → insights-JL1s7Uul.js.map} +1 -1
- package/public/assets/{maintenance-DsszLb92.js → maintenance-CTMfhXFM.js} +2 -2
- package/public/assets/{maintenance-DsszLb92.js.map → maintenance-CTMfhXFM.js.map} +1 -1
- package/public/assets/{managed-agents-DqWPxFE0.js → managed-agents-C4cb3xQ4.js} +2 -2
- package/public/assets/{managed-agents-DqWPxFE0.js.map → managed-agents-C4cb3xQ4.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CWR4ILHK.js → markdown-preview-impl-DQIi0O7r.js} +2 -2
- package/public/assets/{markdown-preview-impl-CWR4ILHK.js.map → markdown-preview-impl-DQIi0O7r.js.map} +1 -1
- package/public/assets/{memory-C2s9bJ38.js → memory-DJPXFdgw.js} +2 -2
- package/public/assets/{memory-C2s9bJ38.js.map → memory-DJPXFdgw.js.map} +1 -1
- package/public/assets/{messages-DHRJNAvy.js → messages-RSD2g4Wx.js} +2 -2
- package/public/assets/{messages-DHRJNAvy.js.map → messages-RSD2g4Wx.js.map} +1 -1
- package/public/assets/{orchestrators-BGNNllJc.js → orchestrators-BGIkULBF.js} +2 -2
- package/public/assets/{orchestrators-BGNNllJc.js.map → orchestrators-BGIkULBF.js.map} +1 -1
- package/public/assets/{overview-C1vZfyhW.js → overview-ByE9V8pe.js} +2 -2
- package/public/assets/{overview-C1vZfyhW.js.map → overview-ByE9V8pe.js.map} +1 -1
- package/public/assets/{pairs-C1uzo8x0.js → pairs-C284ntvD.js} +2 -2
- package/public/assets/{pairs-C1uzo8x0.js.map → pairs-C284ntvD.js.map} +1 -1
- package/public/assets/{security-XhAAP6q3.js → security-vDglScsg.js} +2 -2
- package/public/assets/{security-XhAAP6q3.js.map → security-vDglScsg.js.map} +1 -1
- package/public/assets/{select-DEGUFEjx.js → select-DaOiWDBR.js} +2 -2
- package/public/assets/{select-DEGUFEjx.js.map → select-DaOiWDBR.js.map} +1 -1
- package/public/assets/{settings-DTbVUdlZ.js → settings-DiLcbSqx.js} +2 -2
- package/public/assets/{settings-DTbVUdlZ.js.map → settings-DiLcbSqx.js.map} +1 -1
- package/public/assets/{tasks-zwGo7hon.js → tasks-DWJdJra3.js} +2 -2
- package/public/assets/{tasks-zwGo7hon.js.map → tasks-DWJdJra3.js.map} +1 -1
- package/public/assets/{teams-Brc5D8ox.js → teams-FejXkix4.js} +2 -2
- package/public/assets/{teams-Brc5D8ox.js.map → teams-FejXkix4.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-C6yUAL8G.js → terminal-viewer-impl-Bkt1w42o.js} +2 -2
- package/public/assets/{terminal-viewer-impl-C6yUAL8G.js.map → terminal-viewer-impl-Bkt1w42o.js.map} +1 -1
- package/public/assets/{user-avatar-Bw1R011j.js → user-avatar-ClE2dMD8.js} +2 -2
- package/public/assets/{user-avatar-Bw1R011j.js.map → user-avatar-ClE2dMD8.js.map} +1 -1
- package/public/assets/{work-queue-ge4R7keo.js → work-queue-DZdv9kOb.js} +2 -2
- package/public/assets/{work-queue-ge4R7keo.js.map → work-queue-DZdv9kOb.js.map} +1 -1
- package/public/index.html +2 -2
- package/src/agent-death-diagnosis.ts +255 -0
- package/src/agent-lifecycle-events.ts +42 -19
- package/src/auth/index.ts +23 -0
- package/src/auth/password.ts +122 -0
- package/src/auth/provider.ts +60 -0
- package/src/auth/session.ts +92 -0
- package/src/bus.ts +18 -4
- package/src/db/agents.ts +35 -2
- package/src/db/auth-identities.ts +119 -0
- package/src/db/crash-reports.ts +115 -0
- package/src/db/index.ts +2 -0
- package/src/db/migrations.ts +11 -0
- package/src/index.ts +6 -1
- package/src/maintenance/jobs.ts +6 -1
- package/src/maintenance/teams.ts +68 -1
- package/src/mcp/index.ts +2 -1
- package/src/mcp/tools-messaging.ts +38 -0
- package/src/notification-types.ts +2 -0
- package/src/routes/_shared.ts +40 -2
- package/src/routes/agents.ts +11 -1
- package/src/routes/auth.ts +107 -0
- package/src/routes/commands.ts +21 -3
- package/src/routes/index.ts +10 -1
- package/src/routes/orchestrator.ts +14 -1
- package/src/routes/tokens.ts +8 -2
- package/src/security.ts +1 -1
- package/src/services/terminal-provider-exit.ts +33 -0
- package/src/spawn-targets.ts +19 -8
- package/src/team-idle-tracker.ts +15 -0
- package/src/validation.ts +2 -0
- package/src/workspace-actions.ts +5 -1
- package/src/workspace-auto-merge.ts +87 -4
- package/src/workspace-merge.ts +10 -0
- package/public/assets/index-D8TVUdlC.js +0 -22
- package/public/assets/index-D8TVUdlC.js.map +0 -1
- 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-
|
|
2
|
-
//# sourceMappingURL=user-avatar-
|
|
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-
|
|
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-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
15
|
-
// cleanly (`agent.exited`); one that never
|
|
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).
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// a
|
|
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
|
|
74
|
-
* child that had become ready exited (`agent.exited`);
|
|
75
|
-
* (`agent.spawn_failed`). Routes to the `spawned_by` parent.
|
|
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
|
-
|
|
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
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Composition root for the auth module (#389). Importing this module registers the built-in
|
|
2
|
+
// providers (password in Phase 1; GitHub OAuth joins in Phase 2) and re-exports the public auth API.
|
|
3
|
+
// Consumers import from here so the built-ins are guaranteed registered before any login route runs.
|
|
4
|
+
// This is the ONLY place that wires concrete providers into the registry, so src/auth/provider.ts
|
|
5
|
+
// (the registry) stays free of any dependency on a concrete provider.
|
|
6
|
+
import { registerAuthProvider } from "./provider.ts";
|
|
7
|
+
import { passwordAuthProvider } from "./password.ts";
|
|
8
|
+
|
|
9
|
+
let builtInsRegistered = false;
|
|
10
|
+
|
|
11
|
+
/** Register the built-in auth providers. Idempotent — safe to call from boot and from tests. */
|
|
12
|
+
export function registerBuiltInAuthProviders(): void {
|
|
13
|
+
if (builtInsRegistered) return;
|
|
14
|
+
builtInsRegistered = true;
|
|
15
|
+
registerAuthProvider(passwordAuthProvider);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Side-effect on import: the built-ins are live as soon as anything pulls in the auth module.
|
|
19
|
+
registerBuiltInAuthProviders();
|
|
20
|
+
|
|
21
|
+
export * from "./provider.ts";
|
|
22
|
+
export * from "./password.ts";
|
|
23
|
+
export * from "./session.ts";
|