agent-relay-server 0.38.0 → 0.40.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 (60) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/{activity-ClpDglG8.js → activity-DAz3DcDA.js} +2 -2
  4. package/public/assets/{activity-ClpDglG8.js.map → activity-DAz3DcDA.js.map} +1 -1
  5. package/public/assets/{agents-CHmEJvqV.js → agents-BJ7qRWxt.js} +2 -2
  6. package/public/assets/{agents-CHmEJvqV.js.map → agents-BJ7qRWxt.js.map} +1 -1
  7. package/public/assets/{analytics-2kTjXIj1.js → analytics-MNCS2qnT.js} +2 -2
  8. package/public/assets/{analytics-2kTjXIj1.js.map → analytics-MNCS2qnT.js.map} +1 -1
  9. package/public/assets/{automation-B5U_g-1P.js → automation-COp2Kb3h.js} +2 -2
  10. package/public/assets/{automation-B5U_g-1P.js.map → automation-COp2Kb3h.js.map} +1 -1
  11. package/public/assets/chat-D_O8VqMR.js +2 -0
  12. package/public/assets/chat-D_O8VqMR.js.map +1 -0
  13. package/public/assets/display-ConJ9cJB.js.map +1 -1
  14. package/public/assets/{formatted-body-impl-tmf8IBfr.js → formatted-body-impl-BbMHqkCy.js} +2 -2
  15. package/public/assets/{formatted-body-impl-tmf8IBfr.js.map → formatted-body-impl-BbMHqkCy.js.map} +1 -1
  16. package/public/assets/{index-B1QUkb_O.js → index-DgJOEApM.js} +5 -5
  17. package/public/assets/{index-B1QUkb_O.js.map → index-DgJOEApM.js.map} +1 -1
  18. package/public/assets/{index-Bins8N_5.css → index-Dop-uXiy.css} +1 -1
  19. package/public/assets/{maintenance-Tn23oWBF.js → maintenance-BHv90kXZ.js} +2 -2
  20. package/public/assets/{maintenance-Tn23oWBF.js.map → maintenance-BHv90kXZ.js.map} +1 -1
  21. package/public/assets/managed-agents-BAlvkxfo.js +2 -0
  22. package/public/assets/managed-agents-BAlvkxfo.js.map +1 -0
  23. package/public/assets/{markdown-preview-impl-D4UIjB3I.js → markdown-preview-impl-6k_FWjmd.js} +2 -2
  24. package/public/assets/{markdown-preview-impl-D4UIjB3I.js.map → markdown-preview-impl-6k_FWjmd.js.map} +1 -1
  25. package/public/assets/{memory-SVCob0fo.js → memory-DBjqdVpg.js} +2 -2
  26. package/public/assets/{memory-SVCob0fo.js.map → memory-DBjqdVpg.js.map} +1 -1
  27. package/public/assets/{messages-CHK24Uxx.js → messages-axoaJY2N.js} +2 -2
  28. package/public/assets/{messages-CHK24Uxx.js.map → messages-axoaJY2N.js.map} +1 -1
  29. package/public/assets/{orchestrators-CQcJb6VE.js → orchestrators-DMfHZ44H.js} +2 -2
  30. package/public/assets/{orchestrators-CQcJb6VE.js.map → orchestrators-DMfHZ44H.js.map} +1 -1
  31. package/public/assets/{overview-DbyX7k-7.js → overview-DqxYWpUT.js} +2 -2
  32. package/public/assets/{overview-DbyX7k-7.js.map → overview-DqxYWpUT.js.map} +1 -1
  33. package/public/assets/{pairs-CaL0_ZfW.js → pairs-D3wnzC1V.js} +2 -2
  34. package/public/assets/{pairs-CaL0_ZfW.js.map → pairs-D3wnzC1V.js.map} +1 -1
  35. package/public/assets/{security-BogsfkbT.js → security-KWrb7PKj.js} +2 -2
  36. package/public/assets/{security-BogsfkbT.js.map → security-KWrb7PKj.js.map} +1 -1
  37. package/public/assets/{settings-BOsnUh5f.js → settings-PXMEzNAm.js} +2 -2
  38. package/public/assets/{settings-BOsnUh5f.js.map → settings-PXMEzNAm.js.map} +1 -1
  39. package/public/assets/{tasks-CCxQovOv.js → tasks-CFYShBCz.js} +2 -2
  40. package/public/assets/{tasks-CCxQovOv.js.map → tasks-CFYShBCz.js.map} +1 -1
  41. package/public/assets/{terminal-viewer-impl-BDikdsxs.js → terminal-viewer-impl-BwPYZlWI.js} +2 -2
  42. package/public/assets/{terminal-viewer-impl-BDikdsxs.js.map → terminal-viewer-impl-BwPYZlWI.js.map} +1 -1
  43. package/public/assets/{work-queue-fM-tu0iP.js → work-queue-VQ_6QDJc.js} +2 -2
  44. package/public/assets/{work-queue-fM-tu0iP.js.map → work-queue-VQ_6QDJc.js.map} +1 -1
  45. package/public/index.html +2 -2
  46. package/runner/src/adapter.ts +5 -1
  47. package/src/config-store.ts +1 -1
  48. package/src/db/messages.ts +36 -2
  49. package/src/db/migrations.ts +5 -0
  50. package/src/lifecycle-manager.ts +74 -6
  51. package/src/maintenance.ts +20 -27
  52. package/src/reviewer-pipeline.ts +197 -0
  53. package/src/routes/commands.ts +28 -0
  54. package/src/services/managed-running.ts +1 -0
  55. package/src/services/send-message.ts +4 -0
  56. package/src/workspace-pr-completion.ts +121 -0
  57. package/public/assets/chat-zPXWB-03.js +0 -2
  58. package/public/assets/chat-zPXWB-03.js.map +0 -1
  59. package/public/assets/managed-agents-CasacvJX.js +0 -2
  60. package/public/assets/managed-agents-CasacvJX.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{An as e,Zt as t,at as n,cn as r,i}from"./lucide-react-DLQFnqNm.js";import{i as a,t as o}from"./store-Bo72e9My.js";import{H as s,ct as c,d as l}from"./display-ConJ9cJB.js";import{t as u}from"./badge-JVybSpzR.js";import{t as d}from"./button-BsMqBNJb.js";import{N as f,s as p,z as m}from"./index-B1QUkb_O.js";import{n as h,t as g}from"./card-I8w4U656.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=m(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(n,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(p,{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]?l(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:n,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[e.severity]||c.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)(t,{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)(r,{className:`w-3 h-3 inline mr-1`}),s(n,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(t,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(i,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-fM-tu0iP.js.map
1
+ import{An as e,Zt as t,at as n,cn as r,i}from"./lucide-react-DLQFnqNm.js";import{i as a,t as o}from"./store-Bo72e9My.js";import{H as s,ct as c,d as l}from"./display-ConJ9cJB.js";import{t as u}from"./badge-JVybSpzR.js";import{t as d}from"./button-BsMqBNJb.js";import{N as f,s as p,z as m}from"./index-DgJOEApM.js";import{n as h,t as g}from"./card-I8w4U656.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=m(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(n,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(p,{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]?l(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:n,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[e.severity]||c.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)(t,{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)(r,{className:`w-3 h-3 inline mr-1`}),s(n,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(t,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(i,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-VQ_6QDJc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-fM-tu0iP.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-VQ_6QDJc.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-B1QUkb_O.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-DgJOEApM.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-DQVmg1Zk.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-DLQFnqNm.js">
@@ -35,7 +35,7 @@
35
35
  <link rel="modulepreload" crossorigin href="./assets/badge-JVybSpzR.js">
36
36
  <link rel="modulepreload" crossorigin href="./assets/input-dPzf0luy.js">
37
37
  <link rel="modulepreload" crossorigin href="./assets/themes-mW9zZP4O.js">
38
- <link rel="stylesheet" crossorigin href="./assets/index-Bins8N_5.css">
38
+ <link rel="stylesheet" crossorigin href="./assets/index-Dop-uXiy.css">
39
39
  </head>
40
40
  <body class="bg-background text-foreground antialiased">
41
41
  <div id="root"></div>
@@ -36,13 +36,17 @@ export type ProviderStatusUpdate = SemanticStatus | ProviderStatusEvent;
36
36
  * same lane Claude's transcript capture uses. Provider-independent boundary.
37
37
  */
38
38
  export interface ProviderSessionEvent {
39
- type: "prompt" | "response" | "reasoning" | "tool";
39
+ type: "prompt" | "response" | "narration" | "reasoning" | "tool";
40
40
  body: string;
41
41
  origin?: "chat" | "terminal" | "provider";
42
42
  turnId?: string;
43
43
  label?: string;
44
44
  status?: "running" | "completed" | "failed";
45
45
  streaming?: boolean;
46
+ /** Stable provider-side step id (Codex app-server item id). Carried into
47
+ * MessageSessionMeta.stepId so the server upserts the step's row in place instead of
48
+ * appending a duplicate (a tool's running→completed, a streamed reasoning row). */
49
+ stepId?: string;
46
50
  }
47
51
 
48
52
  export interface ProviderConfig {
@@ -42,7 +42,7 @@ const VALID_PROFILE_ASSET_SOURCES = ["relay", "repo", "inline", "provider"] as c
42
42
  const VALID_PROFILE_FILESYSTEM_SCOPES = ["repo", "workspace", "host"] as const;
43
43
  const VALID_PERMISSION_MODES = ["open", "guarded", "read-only"] as const;
44
44
  const VALID_POLICY_MODES = ["always-on", "on-demand"] as const;
45
- const VALID_MANAGED_STATUSES = ["stopped", "starting", "running", "stopping", "backoff"] as const;
45
+ const VALID_MANAGED_STATUSES = ["stopped", "starting", "running", "stopping", "backoff", "failed"] as const;
46
46
  const BUILT_IN_AGENT_PROFILE_NAMES = new Set(["default-relay", "minimal", "isolated-research"]);
47
47
 
48
48
  const BUILT_IN_AGENT_PROFILES: AgentProfile[] = [
@@ -90,6 +90,34 @@ export function findMessageByIdempotencyKey(from: string, key: string): Message
90
90
  return row ? rowToMessage(row) : null;
91
91
  }
92
92
 
93
+ function sessionStepId(payload: Record<string, unknown> | undefined): string | undefined {
94
+ const session = isRecord(payload?.session) ? payload.session : undefined;
95
+ return stringValue(session?.stepId);
96
+ }
97
+
98
+ /**
99
+ * Codex session-mirror upsert (#parity): a provider step that emits more than once — a
100
+ * tool's running→completed lifecycle, a reasoning/response row that streams — carries a
101
+ * stable `payload.session.stepId`. Instead of appending a second row (which renders as a
102
+ * duplicate on dashboard reload), update the EXISTING row in place: same DB id, so the
103
+ * dashboard's id-keyed merge replaces it. Gated on stepId presence + matching step, so the
104
+ * Claude transcript path (no stepId) is byte-identical. Newer-event-wins via occurredAt so a
105
+ * late retry of an earlier lifecycle phase can't clobber a later one.
106
+ */
107
+ function upsertSessionStepInPlace(existing: Message, input: SendMessageInput, now: number): Message | null {
108
+ const incomingStep = sessionStepId(input.payload);
109
+ if (!incomingStep) return null;
110
+ if (existing.kind !== "session") return null;
111
+ if (sessionStepId(existing.payload) !== incomingStep) return null;
112
+ const incomingAt = sanitizeOccurredAt(input.occurredAt, now) ?? now;
113
+ const existingAt = existing.occurredAt ?? existing.createdAt;
114
+ if (incomingAt < existingAt) return null; // stale lifecycle phase — keep the newer row, no re-emit
115
+ getDb()
116
+ .query("UPDATE messages SET body = ?, payload = ?, occurred_at = ? WHERE id = ?")
117
+ .run(input.body, JSON.stringify(input.payload ?? {}), incomingAt, existing.id);
118
+ return getMessage(existing.id);
119
+ }
120
+
93
121
  export function policyNameFromTarget(target: string): string | null {
94
122
  if (!target.startsWith("policy:")) return null;
95
123
  const name = target.slice("policy:".length).trim();
@@ -169,7 +197,7 @@ export function sanitizeOccurredAt(occurredAt: number | undefined, receivedAt: n
169
197
  return Math.floor(occurredAt);
170
198
  }
171
199
 
172
- export function sendMessageWithResult(input: SendMessageInput): { message: Message; created: boolean } {
200
+ export function sendMessageWithResult(input: SendMessageInput): { message: Message; created: boolean; updated?: boolean } {
173
201
  const now = Date.now();
174
202
  const payload = input.payload ?? {};
175
203
  const attachmentRefs = cleanAttachmentRefs(payload);
@@ -181,7 +209,13 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
181
209
 
182
210
  if (input.idempotencyKey) {
183
211
  const existing = findMessageByIdempotencyKey(input.from, input.idempotencyKey);
184
- if (existing) return { message: existing, created: false };
212
+ if (existing) {
213
+ // Session-step upsert: a re-emitted provider step (tool running→completed, streamed
214
+ // reasoning/response) updates its row in place instead of being dropped as a dup retry.
215
+ const upserted = upsertSessionStepInPlace(existing, input, now);
216
+ if (upserted) return { message: upserted, created: false, updated: true };
217
+ return { message: existing, created: false };
218
+ }
185
219
  }
186
220
 
187
221
  // Resolve thread: if replying, inherit from parent; reject unknown replyTo
@@ -245,6 +245,11 @@ export function applyMigrations(): void {
245
245
  }
246
246
  getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_kind ON messages(kind)");
247
247
  getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_kind_id ON messages(from_agent, kind, id)");
248
+ // (from_agent, idempotency_key) powers findMessageByIdempotencyKey — the retry-dedup lookup
249
+ // AND the session-step in-place upsert (Codex tool running→completed / streamed reasoning),
250
+ // which runs per persisted session step. Added here (not the base schema) because the column
251
+ // is migration-added for pre-hardening DBs. Without it that lookup full-scans messages.
252
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key)");
248
253
 
249
254
  // Backfill thread_id for pre-migration rows (self-threaded).
250
255
  getDb().run("UPDATE messages SET thread_id = id WHERE thread_id IS NULL");
@@ -1,4 +1,5 @@
1
1
  import { createCommand } from "./commands-db";
2
+ import { extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
2
3
  import { isPathWithinBase } from "./utils";
3
4
  import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces } from "./db";
4
5
  import {
@@ -21,6 +22,8 @@ const DEFAULT_TICK_MS = 10_000;
21
22
  // orchestrator never confirms must not strand it in "stopping" forever.
22
23
  const START_TIMEOUT_MS = 120_000;
23
24
  const STOP_TIMEOUT_MS = 60_000;
25
+ const FAST_FAIL_RUNTIME_MS = 30_000;
26
+ const MAX_FAST_FAILURES = 3;
24
27
  const DAY_MS = 24 * 60 * 60 * 1000;
25
28
  const DEFAULT_COMPACT_THRESHOLD = 0.75;
26
29
  const DEFAULT_COMPACT_TARGET = 0.3;
@@ -135,7 +138,7 @@ export class LifecycleManager {
135
138
  (state.tmuxSession && agent.tmuxSession === state.tmuxSession) ||
136
139
  (agent.policyName === policy.name && (!state.spawnRequestId || agent.spawnRequestId === state.spawnRequestId))
137
140
  ));
138
- this.markBackoff(policy, state, exited?.lastError ?? "orchestrator session disappeared");
141
+ this.markPolicyFailure(policy, state, exited?.lastError ?? "orchestrator session disappeared", exited);
139
142
  } else if (state.status === "stopping" && !reported) {
140
143
  const next = updateManagedAgentState(policy.name, {
141
144
  status: "stopped",
@@ -166,7 +169,7 @@ export class LifecycleManager {
166
169
  if (next) this.emitState(next);
167
170
  return;
168
171
  }
169
- this.markBackoff(policy, state, "agent disappeared");
172
+ this.markPolicyFailure(policy, state, "agent disappeared");
170
173
  }
171
174
 
172
175
  onConfigChanged(namespace: string, key: string): void {
@@ -318,7 +321,7 @@ export class LifecycleManager {
318
321
 
319
322
  if (state.status === "starting") {
320
323
  if (state.lastSpawnAt && this.now() - state.lastSpawnAt > START_TIMEOUT_MS) {
321
- this.markBackoff(policy, state, "spawn timed out before registration");
324
+ this.markPolicyFailure(policy, state, "spawn timed out before registration");
322
325
  }
323
326
  return;
324
327
  }
@@ -343,7 +346,7 @@ export class LifecycleManager {
343
346
  return;
344
347
  }
345
348
  if (!agent || agent.status === "offline") {
346
- this.markBackoff(policy, state, "agent offline");
349
+ this.markPolicyFailure(policy, state, agentTerminalFailureMessage(agent) ?? "agent offline");
347
350
  return;
348
351
  }
349
352
  this.resetBackoffAfterHealthyRun(policy, state);
@@ -373,6 +376,8 @@ export class LifecycleManager {
373
376
  return;
374
377
  }
375
378
 
379
+ if (state.status === "failed") return;
380
+
376
381
  if (state.status === "stopped") {
377
382
  if (enabled && (policy.mode === "always-on" || this.hasQueuedMessages(policy.name))) this.spawnAgent(policy, policy.mode === "always-on" ? "always-on" : "message-trigger");
378
383
  }
@@ -408,9 +413,23 @@ export class LifecycleManager {
408
413
  this.emitState(state);
409
414
  }
410
415
 
411
- private markBackoff(policy: SpawnPolicy, state: ManagedAgentState, error: string): void {
416
+ private markPolicyFailure(policy: SpawnPolicy, state: ManagedAgentState, error: string, exited?: ManagedSessionExitDiagnostics): void {
412
417
  if (state.status === "stopping") return;
418
+ const modelUnavailable = managedModelUnavailableMessage(policy, error, exited);
419
+ if (modelUnavailable) {
420
+ this.markFailed(policy, state, modelUnavailable);
421
+ return;
422
+ }
413
423
  const failures = state.consecutiveFailures + 1;
424
+ if (isFastFailStorm(failures, exited)) {
425
+ this.markFailed(policy, state, error);
426
+ return;
427
+ }
428
+ this.markBackoff(policy, state, error, failures);
429
+ }
430
+
431
+ private markBackoff(policy: SpawnPolicy, state: ManagedAgentState, error: string, failures = state.consecutiveFailures + 1): void {
432
+ if (state.status === "stopping") return;
414
433
  const delay = this.backoffDelay(policy, state);
415
434
  const next = upsertManagedAgentState({
416
435
  policyName: policy.name,
@@ -419,6 +438,7 @@ export class LifecycleManager {
419
438
  orchestratorId: policy.orchestratorId,
420
439
  provider: policy.provider,
421
440
  spawnRequestId: state.spawnRequestId,
441
+ tmuxSession: state.tmuxSession,
422
442
  lastSpawnAt: state.lastSpawnAt,
423
443
  restartCount: state.restartCount,
424
444
  consecutiveFailures: failures,
@@ -428,6 +448,25 @@ export class LifecycleManager {
428
448
  this.emitState(next);
429
449
  }
430
450
 
451
+ private markFailed(policy: SpawnPolicy, state: ManagedAgentState, error: string): void {
452
+ const failures = state.consecutiveFailures + 1;
453
+ const next = upsertManagedAgentState({
454
+ policyName: policy.name,
455
+ status: "failed",
456
+ agentId: undefined,
457
+ orchestratorId: policy.orchestratorId,
458
+ provider: policy.provider,
459
+ spawnRequestId: state.spawnRequestId,
460
+ tmuxSession: state.tmuxSession,
461
+ lastSpawnAt: state.lastSpawnAt,
462
+ restartCount: state.restartCount,
463
+ consecutiveFailures: failures,
464
+ backoffUntil: undefined,
465
+ lastError: error,
466
+ });
467
+ this.emitState(next);
468
+ }
469
+
431
470
  private backoffDelay(policy: SpawnPolicy, state: ManagedAgentState | null): number {
432
471
  const schedule = policy.backoff.schedule.length ? policy.backoff.schedule : [30];
433
472
  const index = Math.min(state?.consecutiveFailures ?? 0, schedule.length - 1);
@@ -586,10 +625,39 @@ export function __resetLifecycleManager(): void {
586
625
  singleton = null;
587
626
  }
588
627
 
628
+ function managedModelUnavailableMessage(policy: SpawnPolicy, error: string, exited?: ManagedSessionExitDiagnostics): string | null {
629
+ if (policy.provider !== "claude" && exited?.provider !== "claude") return null;
630
+ return extractClaudeModelUnavailableMessage([
631
+ error,
632
+ exited?.lastError,
633
+ ...(exited?.logTail ?? []),
634
+ ...(exited?.unavailable ?? []),
635
+ ].join("\n"));
636
+ }
637
+
638
+ function isFastFailStorm(failures: number, exited?: ManagedSessionExitDiagnostics): boolean {
639
+ return failures >= MAX_FAST_FAILURES && exited !== undefined && exited.runtimeMs < FAST_FAIL_RUNTIME_MS;
640
+ }
641
+
642
+ function agentTerminalFailureMessage(agent: ReturnType<typeof getAgent> | null): string | null {
643
+ if (!agent?.meta) return null;
644
+ const reason = typeof agent.meta.terminalFailureReason === "string" ? agent.meta.terminalFailureReason : undefined;
645
+ const message = typeof agent.meta.terminalFailureMessage === "string" ? agent.meta.terminalFailureMessage : undefined;
646
+ if (reason === "model-unavailable" && message) return message;
647
+ const providerState = isPlainRecord(agent.meta.providerState) ? agent.meta.providerState : undefined;
648
+ const stateReason = typeof providerState?.reason === "string" ? providerState.reason : undefined;
649
+ const stateMessage = typeof providerState?.message === "string" ? providerState.message : undefined;
650
+ if (stateReason === "model-unavailable" && stateMessage) return stateMessage;
651
+ return null;
652
+ }
653
+
654
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
655
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
656
+ }
657
+
589
658
  function alwaysReloadTags(tags: string[]): string[] {
590
659
  return tags
591
660
  .filter((tag) => tag.startsWith("memory-reload:"))
592
661
  .map((tag) => tag.slice("memory-reload:".length).trim())
593
662
  .filter(Boolean);
594
663
  }
595
-
@@ -34,6 +34,8 @@ import {
34
34
  import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "./types";
35
35
  import { requestWorkspaceMerge } from "./workspace-merge";
36
36
  import { reconcileLandedWorkspace } from "./branch-landed";
37
+ import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
38
+ import { preparePrCompletionScan } from "./workspace-pr-completion";
37
39
  import { notifyAgentOffline } from "./agent-lifecycle-events";
38
40
  import { workspaceActiveClaim } from "./workspace-claim";
39
41
  import { reapOrphanedWorktrees } from "./workspace-orphans";
@@ -577,10 +579,8 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
577
579
  const candidates = listWorkspaces().filter(
578
580
  (ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && CONFLICT_SCAN_STATUSES.has(ws.status),
579
581
  );
580
- const flagged: string[] = [];
581
- const cleared: string[] = [];
582
- const merged: string[] = [];
583
- const notifiedStewards: string[] = [];
582
+ const flagged: string[] = [], cleared: string[] = [], merged: string[] = [], notifiedStewards: string[] = [];
583
+ const reviewerArmed: string[] = [], reviewerChangesRequested: string[] = [], relayPrMerged: string[] = [], relayPrWaiting: string[] = [];
584
584
 
585
585
  for (const ws of candidates) {
586
586
  const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
@@ -608,19 +608,9 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
608
608
  if (patched && deriveBranchState(patched) !== before) emitWorkspaceChange(patched);
609
609
  }
610
610
 
611
- // Landing wins over everything else. Once the work is in base whether the
612
- // PR was squash/cherry-pick merged on GitHub or fast-forwarded locally the
613
- // workspace is done, even if `git merge-tree` still predicts a textual
614
- // conflict against the now-moved base (a PR-strategy row sits at
615
- // merge_planned forever otherwise, and the conflict scan can even pin a
616
- // landed branch to `conflict`). Reconcile to the terminal `merged` status so
617
- // the dashboard stops showing it as unmerged and GC prunes it on schedule.
618
- // This runs BEFORE the conflict-undefined skip below: a PR merged via a regular
619
- // merge commit makes the branch an ancestor (ahead=0 → no-op → conflict comes
620
- // back undefined), which the skip would otherwise drop, stranding the row at
621
- // merge_planned — the exact #304 stall.
622
- // Reconcile + finalize (record SHA, fire branch.landed) in branch-landed.ts so the
623
- // giant doesn't grow (#291) and land-notify stays single-homed (#304).
611
+ // Landing wins over conflict checks. A merged PR can look like a no-op or conflict
612
+ // locally, so reconcile before the conflict-undefined skip and keep finalization in
613
+ // branch-landed.ts (#304).
624
614
  const landed = p.landed === true || p.prMerged === true;
625
615
  if (landed && LANDED_RECONCILE_STATUSES.has(ws.status)) {
626
616
  reconcileLandedWorkspace(ws, p);
@@ -628,6 +618,13 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
628
618
  continue;
629
619
  }
630
620
 
621
+ const prAction = preparePrCompletionScan(ws, p, orch.agentId);
622
+ if (prAction.armCommand) { emitCommand(prAction.armCommand); reviewerArmed.push(ws.id); continue; }
623
+ if (prAction.notifiedOwner) { reviewerChangesRequested.push(ws.id); continue; }
624
+ if (prAction.mergeCommand) { emitCommand(prAction.mergeCommand); relayPrMerged.push(ws.id); continue; }
625
+ if (prAction.waitReason) relayPrWaiting.push(`${ws.id}:${prAction.waitReason}`);
626
+ if (prAction.stop) continue;
627
+
631
628
  // Past here we act on the conflict signal — skip when the host couldn't assess it
632
629
  // (undefined): never flag/clear a conflict on incomplete data.
633
630
  if (p.conflict === undefined) continue;
@@ -681,17 +678,11 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
681
678
  }
682
679
  }
683
680
 
684
- return { scanned: candidates.length, flagged, cleared, merged, notifiedStewards };
681
+ return { scanned: candidates.length, flagged, cleared, merged, notifiedStewards, reviewerArmed, reviewerChangesRequested, relayPrMerged, relayPrWaiting };
685
682
  }
686
683
 
687
- // Deterministic auto-land (Layer 0, issue #167 / #207 / #242). Walk the "ready to
688
- // land" queue (isolated worktrees in any READY_TO_LAND status `ready` from
689
- // `relay_workspace_ready`, or `review_requested` from a failed-merge retry) and
690
- // land any whose merge is predicted conflict-free, via the shared lease-serialized
691
- // merge helper — even
692
- // when the base moved on (behind>0): mergeRebaseFf rebases onto the current base
693
- // before fast-forwarding. Only a predicted conflict or an unknown merge state is
694
- // left for the steward; clean parallel work lands with no agent in the loop.
684
+ // Deterministic auto-land (Layer 0, #167/#207/#242): land conflict-free ready
685
+ // work under the shared lease, rebasing when base moved; leave uncertainty to stewards.
695
686
  async function autoMergeCleanFastForwards(): Promise<Record<string, unknown>> {
696
687
  if (process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE === "0") return { skipped: "disabled" };
697
688
  const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
@@ -704,10 +695,12 @@ async function autoMergeCleanFastForwards(): Promise<Record<string, unknown>> {
704
695
  const merged: string[] = [];
705
696
  const heldByLease: string[] = [];
706
697
  const heldByClaim: string[] = [];
698
+ const heldAwaitingApproval: string[] = [];
707
699
  const leftForSteward: string[] = [];
708
700
  const wokeStewards: string[] = [];
709
701
 
710
702
  for (const ws of candidates) {
703
+ if (isAwaitingReviewerApproval(ws)) { heldAwaitingApproval.push(ws.id); continue; }
711
704
  // A claimed workspace is being validated by a steward — don't race it (#208).
712
705
  if (workspaceActiveClaim(ws)) { heldByClaim.push(ws.id); continue; }
713
706
  const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
@@ -764,7 +757,7 @@ async function autoMergeCleanFastForwards(): Promise<Record<string, unknown>> {
764
757
  });
765
758
  }
766
759
 
767
- return { scanned: candidates.length, merged, heldByLease, heldByClaim, leftForSteward, wokeStewards };
760
+ return { scanned: candidates.length, merged, heldByLease, heldByClaim, heldAwaitingApproval, leftForSteward, wokeStewards };
768
761
  }
769
762
 
770
763
  // Send a system DM, swallowing failures (a stale/missing/misconfigured target