agent-relay-server 0.36.1 → 0.37.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 (65) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +1 -1
  3. package/public/assets/{activity-C3mkM6AU.js → activity-BgkmA1lh.js} +2 -2
  4. package/public/assets/{activity-C3mkM6AU.js.map → activity-BgkmA1lh.js.map} +1 -1
  5. package/public/assets/{agents-CAhQO7JH.js → agents-B7HnuAXx.js} +2 -2
  6. package/public/assets/{agents-CAhQO7JH.js.map → agents-B7HnuAXx.js.map} +1 -1
  7. package/public/assets/{analytics-BwihhhNn.js → analytics-0-akxJCJ.js} +2 -2
  8. package/public/assets/{analytics-BwihhhNn.js.map → analytics-0-akxJCJ.js.map} +1 -1
  9. package/public/assets/{automation-BLXToUiU.js → automation-CaE1z_-M.js} +2 -2
  10. package/public/assets/{automation-BLXToUiU.js.map → automation-CaE1z_-M.js.map} +1 -1
  11. package/public/assets/{chat-8iIPyww9.js → chat-BANKUW05.js} +2 -2
  12. package/public/assets/{chat-8iIPyww9.js.map → chat-BANKUW05.js.map} +1 -1
  13. package/public/assets/{formatted-body-impl-BHH0wzY7.js → formatted-body-impl-DExNPNsL.js} +2 -2
  14. package/public/assets/{formatted-body-impl-BHH0wzY7.js.map → formatted-body-impl-DExNPNsL.js.map} +1 -1
  15. package/public/assets/{index-CaauKXl9.js → index-DEZdON6c.js} +5 -5
  16. package/public/assets/{index-CaauKXl9.js.map → index-DEZdON6c.js.map} +1 -1
  17. package/public/assets/{maintenance-9n_rJCHT.js → maintenance-DpTdJxQp.js} +2 -2
  18. package/public/assets/{maintenance-9n_rJCHT.js.map → maintenance-DpTdJxQp.js.map} +1 -1
  19. package/public/assets/{managed-agents-Rp2-xpBx.js → managed-agents-B3df2xfk.js} +2 -2
  20. package/public/assets/{managed-agents-Rp2-xpBx.js.map → managed-agents-B3df2xfk.js.map} +1 -1
  21. package/public/assets/{markdown-preview-impl-YfJsGh6I.js → markdown-preview-impl-D0Zj7c3T.js} +2 -2
  22. package/public/assets/{markdown-preview-impl-YfJsGh6I.js.map → markdown-preview-impl-D0Zj7c3T.js.map} +1 -1
  23. package/public/assets/{memory-BQONtGQS.js → memory-TATN2vZf.js} +2 -2
  24. package/public/assets/{memory-BQONtGQS.js.map → memory-TATN2vZf.js.map} +1 -1
  25. package/public/assets/{messages-DGqpkH72.js → messages-3rS1lxIf.js} +2 -2
  26. package/public/assets/{messages-DGqpkH72.js.map → messages-3rS1lxIf.js.map} +1 -1
  27. package/public/assets/{orchestrators-b8k9QoGv.js → orchestrators-CRIV0g5y.js} +2 -2
  28. package/public/assets/{orchestrators-b8k9QoGv.js.map → orchestrators-CRIV0g5y.js.map} +1 -1
  29. package/public/assets/{overview-DSU_CggA.js → overview-CmHC_5oM.js} +2 -2
  30. package/public/assets/{overview-DSU_CggA.js.map → overview-CmHC_5oM.js.map} +1 -1
  31. package/public/assets/{pairs-DGocNC1U.js → pairs-DBPAhXTI.js} +2 -2
  32. package/public/assets/{pairs-DGocNC1U.js.map → pairs-DBPAhXTI.js.map} +1 -1
  33. package/public/assets/{security-BSh0QxOl.js → security-572X5MNX.js} +2 -2
  34. package/public/assets/{security-BSh0QxOl.js.map → security-572X5MNX.js.map} +1 -1
  35. package/public/assets/{settings-C03CAJgO.js → settings-vTBu8w3O.js} +2 -2
  36. package/public/assets/{settings-C03CAJgO.js.map → settings-vTBu8w3O.js.map} +1 -1
  37. package/public/assets/{tasks-rKbuUPOk.js → tasks-C0bPrDgN.js} +2 -2
  38. package/public/assets/{tasks-rKbuUPOk.js.map → tasks-C0bPrDgN.js.map} +1 -1
  39. package/public/assets/{terminal-viewer-impl-CA8u4jh3.js → terminal-viewer-impl-rVPA6Fsx.js} +2 -2
  40. package/public/assets/{terminal-viewer-impl-CA8u4jh3.js.map → terminal-viewer-impl-rVPA6Fsx.js.map} +1 -1
  41. package/public/assets/{work-queue-DOsA9s4M.js → work-queue-BxkpTt_A.js} +2 -2
  42. package/public/assets/{work-queue-DOsA9s4M.js.map → work-queue-BxkpTt_A.js.map} +1 -1
  43. package/public/index.html +1 -1
  44. package/runner/src/adapter.ts +7 -0
  45. package/scripts/orchestrator-spawn-smoke.ts +65 -33
  46. package/src/agent-ref.ts +28 -1
  47. package/src/bus.ts +51 -35
  48. package/src/compaction-watch.ts +7 -0
  49. package/src/index.ts +23 -6
  50. package/src/lifecycle-manager.ts +33 -71
  51. package/src/mcp.ts +100 -308
  52. package/src/routes/agent-sessions.ts +38 -3
  53. package/src/routes/agents-spawn.ts +43 -175
  54. package/src/routes/commands.ts +7 -19
  55. package/src/routes/messages.ts +24 -87
  56. package/src/security.ts +15 -0
  57. package/src/services/auth-context.ts +109 -0
  58. package/src/services/dispatch-command.ts +60 -0
  59. package/src/services/errors.ts +26 -0
  60. package/src/services/managed-running.ts +130 -0
  61. package/src/services/parity-harness.ts +135 -0
  62. package/src/services/register-agent.ts +74 -0
  63. package/src/services/send-message.ts +159 -0
  64. package/src/services/shutdown-agent.ts +234 -0
  65. package/src/services/spawn-agent.ts +278 -0
@@ -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-DKVWC6Uh.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{M as f,R as p,s as m}from"./index-CaauKXl9.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=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&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)(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]?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-DOsA9s4M.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-DKVWC6Uh.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{M as f,R as p,s as m}from"./index-DEZdON6c.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=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&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)(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]?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-BxkpTt_A.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-DOsA9s4M.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-BxkpTt_A.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-CaauKXl9.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-DEZdON6c.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">
@@ -157,6 +157,13 @@ export interface ProviderAdapter {
157
157
  // `options.readyTimeoutMs` lets the runner widen the provider-ready wait for the
158
158
  // first (cold-start) delivery vs. a fast re-attempt after a ready signal (#329).
159
159
  deliverInitialPrompt?(process: ManagedProcess, prompt: string, options?: { readyTimeoutMs?: number }): Promise<void>;
160
+ // When true, the adapter seeds the spawn-time initial prompt as a launch argument
161
+ // (Claude's positional `claude "<prompt>"`), so it's already delivered the instant the
162
+ // session starts. The runner must then NOT also deliver it post-launch via
163
+ // deliverInitialPrompt — that would double-deliver and re-introduce the send-keys
164
+ // onboarding race (#352). deliverInitialPrompt stays available for mid-session injection
165
+ // (dashboard chat box) and ongoing message delivery, which have no launch-arg equivalent.
166
+ seedsInitialPromptAtLaunch?: boolean;
160
167
  deliver(process: ManagedProcess, messages: Message[]): Promise<void>;
161
168
  onStatusChange(cb: (status: ProviderStatusUpdate) => void): void;
162
169
  // Subscribe to session-mirror events from providers that emit them directly
@@ -125,47 +125,79 @@ for (const provider of providers) {
125
125
 
126
126
  const label = `smoke-${provider}-${Date.now()}`;
127
127
  const startedAt = Date.now();
128
+ // #352: for providers seeded with an initial prompt, assert the spawn prompt was actually
129
+ // DELIVERED and the first turn RAN — by making the prompt's first action a relay tool call
130
+ // (send a nonce message) and waiting for that nonce to land in the relay feed. Registration
131
+ // alone (the old smoke) happens via the plugin regardless of prompt delivery, so it never
132
+ // caught the silently-dropped spawn prompt. The nonce is the end-to-end proof.
133
+ const nonce = `SMOKE-NONCE-${provider}-${Date.now()}-${Math.round(Math.random() * 1e9).toString(36)}`;
134
+ const promptProvider = provider === "claude";
135
+ const prompt = `Agent Relay spawn smoke. As your VERY FIRST action, use the agent-relay MCP tool relay_send_message to send to "label:${label}" with body exactly "${nonce}". Then wait for shutdown — do nothing else.`;
128
136
  console.log(`spawn ${provider} via ${orchestrator.id}`);
129
137
  await api("POST", `/agents/spawn`, {
130
138
  provider,
131
139
  orchestratorId: orchestrator.id,
132
140
  cwd,
133
141
  label,
134
- approvalMode: "guarded",
135
- ...(provider === "claude" ? { prompt: "Agent Relay spawn smoke. Register with relay and wait for shutdown." } : {}),
142
+ // Prompt-seeded providers must run UNATTENDED: under guarded mode the first-turn relay tool
143
+ // call would block on a dashboard approval that never comes here. Autonomous workers spawn
144
+ // open for the same reason. (Approval mode is orthogonal to prompt delivery, which is the
145
+ // regression under test.)
146
+ approvalMode: promptProvider ? "open" : "guarded",
147
+ ...(promptProvider ? { prompt } : {}),
136
148
  });
137
149
 
138
- const agent = await waitFor(`waiting for ${provider} agent registration`, async () => {
139
- const agents = await api<Agent[]>("GET", "/agents");
140
- return findSpawnedAgent(agents, provider, label, startedAt);
141
- });
142
- console.log(`registered ${provider}: ${agent.id}`);
143
-
144
- const managed = await waitFor(`waiting for ${provider} managed session`, async () => {
145
- const latest = (await api<Orchestrator[]>("GET", "/orchestrators")).find((orch) => orch.id === orchestrator.id);
146
- return latest?.managedAgents?.find((entry) => entry.label === label || entry.tmuxSession.includes(label)) || null;
147
- });
148
-
149
- await api("POST", `/agents/${encodeURIComponent(agent.id)}/actions`, {
150
- action: "shutdown",
151
- });
152
- await waitFor(`waiting for ${provider} agent cleanup`, async () => {
153
- const current = await apiOptional<Agent>("GET", `/agents/${encodeURIComponent(agent.id)}`);
154
- return !current || (current.status === "offline" && current.ready === false) ? true : null;
155
- });
156
- await apiOptional("DELETE", `/agents/${encodeURIComponent(agent.id)}`);
157
- await api("POST", `/orchestrators/${encodeURIComponent(orchestrator.id)}/actions`, {
158
- action: "shutdown",
159
- agentId: agent.id,
160
- tmuxSession: managed.tmuxSession,
161
- reason: "smoke-cleanup",
162
- });
163
- await waitFor(`waiting for ${provider} managed session cleanup`, async () => {
164
- const latest = (await api<Orchestrator[]>("GET", "/orchestrators")).find((orch) => orch.id === orchestrator.id);
165
- const stillManaged = latest?.managedAgents?.some((entry) => entry.tmuxSession === managed.tmuxSession || entry.agentId === agent.id);
166
- return stillManaged ? null : true;
167
- });
168
- console.log(`shutdown ${provider}: ${label}`);
150
+ let agent: Agent | undefined;
151
+ let managed: { tmuxSession: string } | undefined;
152
+ try {
153
+ agent = await waitFor(`waiting for ${provider} agent registration`, async () => {
154
+ const agents = await api<Agent[]>("GET", "/agents");
155
+ return findSpawnedAgent(agents, provider, label, startedAt);
156
+ });
157
+ console.log(`registered ${provider}: ${agent.id}`);
158
+
159
+ managed = await waitFor(`waiting for ${provider} managed session`, async () => {
160
+ const latest = (await api<Orchestrator[]>("GET", "/orchestrators")).find((orch) => orch.id === orchestrator.id);
161
+ return latest?.managedAgents?.find((entry) => entry.label === label || entry.tmuxSession.includes(label)) || null;
162
+ });
163
+
164
+ // Hard-assert the seeded first turn ran and reached relay (the prompt-delivery regression guard).
165
+ if (promptProvider) {
166
+ await waitFor(`waiting for ${provider} spawn-prompt first turn (nonce relay tool call)`, async () => {
167
+ const feed = await api<Array<{ body?: string }>>("GET", "/messages?limit=80");
168
+ return feed.some((m) => (m.body || "").includes(nonce)) ? true : null;
169
+ });
170
+ console.log(`spawn-prompt delivered + first turn called relay ${provider}: ${nonce}`);
171
+ }
172
+ } finally {
173
+ // Always tear down — a failed assertion (e.g. a re-introduced prompt-drop) must NOT leak a
174
+ // managed agent on the host running the gate. Best-effort: teardown errors don't mask the
175
+ // original failure.
176
+ if (agent) {
177
+ const id = agent.id;
178
+ await api("POST", `/agents/${encodeURIComponent(id)}/actions`, { action: "shutdown" }).catch(() => {});
179
+ await waitFor(`waiting for ${provider} agent cleanup`, async () => {
180
+ const current = await apiOptional<Agent>("GET", `/agents/${encodeURIComponent(id)}`);
181
+ return !current || (current.status === "offline" && current.ready === false) ? true : null;
182
+ }).catch(() => {});
183
+ await apiOptional("DELETE", `/agents/${encodeURIComponent(id)}`).catch(() => {});
184
+ if (managed) {
185
+ const tmuxSession = managed.tmuxSession;
186
+ await api("POST", `/orchestrators/${encodeURIComponent(orchestrator.id)}/actions`, {
187
+ action: "shutdown",
188
+ agentId: id,
189
+ tmuxSession,
190
+ reason: "smoke-cleanup",
191
+ }).catch(() => {});
192
+ await waitFor(`waiting for ${provider} managed session cleanup`, async () => {
193
+ const latest = (await api<Orchestrator[]>("GET", "/orchestrators")).find((orch) => orch.id === orchestrator.id);
194
+ const stillManaged = latest?.managedAgents?.some((entry) => entry.tmuxSession === tmuxSession || entry.agentId === id);
195
+ return stillManaged ? null : true;
196
+ }).catch(() => {});
197
+ }
198
+ console.log(`shutdown ${provider}: ${label}`);
199
+ }
200
+ }
169
201
  }
170
202
 
171
203
  console.log("spawn smoke passed");
package/src/agent-ref.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  // never silently picks among several — it reports candidates instead.
13
13
 
14
14
  import { STALE_TTL_MS } from "./config";
15
- import type { AgentCard, Message } from "./types";
15
+ import type { AgentCard, Message, TokenConstraints } from "./types";
16
16
 
17
17
  interface ResolveOptions {
18
18
  /** Exclude this agent id from matches (e.g. the requester, when pairing). */
@@ -255,6 +255,33 @@ export function planSend(to: string, agents: AgentCard[], opts: ResolveOptions =
255
255
  return { kind: "not_found", message: notFoundMessage(target, agents) };
256
256
  }
257
257
 
258
+ /**
259
+ * THE caller-identity resolver: the agent id behind a token's signed constraints, for
260
+ * `from`-autofill, whoami, and spawn/shutdown gating (#221/#243/#323). Two cases, in order:
261
+ * - an identity-bearing token (`constraints.agents` with a single id) → that id, no db.
262
+ * - a managed/spawned agent's runner token (no `agents` constraint, but a single
263
+ * `spawnRequestId` or `policy`) → matched back to its registered agent card.
264
+ * Returns undefined for admin/server tokens (no constraints → unrestricted by design) and
265
+ * multi-agent tokens. `getAgents` is LAZY — the live-agent scan runs only when a single
266
+ * spawnRequestId/policy actually needs resolving, so identity-bearing and admin tokens (and
267
+ * the hot registration path) pay no db cost. One home so the #243 "managed agents silently
268
+ * lose implicit identity" drift can't recur across the transport-convergence services.
269
+ */
270
+ export function resolveCallerAgentId(
271
+ constraints: TokenConstraints | undefined,
272
+ getAgents: () => AgentCard[],
273
+ ): string | undefined {
274
+ const agents = constraints?.agents;
275
+ if (agents?.length === 1) return agents[0];
276
+ const spawnRequestId = constraints?.spawnRequestIds?.length === 1 ? constraints.spawnRequestIds[0] : undefined;
277
+ const policyName = constraints?.policies?.length === 1 ? constraints.policies[0] : undefined;
278
+ if (!spawnRequestId && !policyName) return undefined;
279
+ const match = getAgents().find((a) =>
280
+ (spawnRequestId !== undefined && a.meta?.spawnRequestId === spawnRequestId) ||
281
+ (policyName !== undefined && a.meta?.policyName === policyName));
282
+ return match?.id;
283
+ }
284
+
258
285
  function fanoutReceipt(recipients: string[]): DeliveryReceipt {
259
286
  if (recipients.length === 0) return { delivered: false, expectReply: false, recipients: [], queued: true, reason: "no online members — queued" };
260
287
  return { delivered: true, expectReply: true, recipients };
package/src/bus.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  import type { Server, ServerWebSocket } from "bun";
2
- import { createActivityEvent, getAgent, getDb, heartbeat, markReady, mergeAgentMeta, orphanTasksForAgent, revokeRuntimeTokensForAgent, setStatus, upsertAgent, validateAgentSession } from "./db";
2
+ import { ValidationError, createActivityEvent, getAgent, getDb, heartbeat, markReady, mergeAgentMeta, orphanTasksForAgent, revokeRuntimeTokensForAgent, setStatus, validateAgentSession } from "./db";
3
3
  import { getOldestOutboxCursor, getOutboxCursor, replayEvents, type BusEvent } from "./bus-outbox";
4
4
  import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
5
- import { createCommand, getCommand, updateCommand } from "./commands-db";
5
+ import { getCommand, updateCommand } from "./commands-db";
6
6
  import { emitCommandEvent } from "./command-events";
7
7
  import { getLifecycleManager } from "./lifecycle-manager";
8
8
  import { noteAgentTimelineEvent, noteCompactionCommandCompleted } from "./compaction-watch";
9
+ import { registerAgent } from "./services/register-agent";
10
+ import { authContextFromBus } from "./services/auth-context";
11
+ import { commandAuthorizationResource, dispatchCommand } from "./services/dispatch-command";
12
+ import { ServiceAuthError } from "./services/errors";
13
+ import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
9
14
  import { applyCommandToRecipe } from "./recipe-runner";
10
15
  import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
11
16
  import {
@@ -17,7 +22,7 @@ import {
17
22
  } from "agent-relay-sdk/protocol";
18
23
  import { errMessage, isRecord, stringValue } from "agent-relay-sdk";
19
24
  import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
20
- import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, Task } from "./types";
25
+ import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, RegisterAgentInput, Task } from "./types";
21
26
 
22
27
  interface BusSocketData {
23
28
  kind: "bus";
@@ -219,19 +224,42 @@ function handleCommandFrame(
219
224
  return;
220
225
  }
221
226
 
222
- if (!busCommandAuthorized(conn, { target, params })) {
223
- sendCommandResult(ws, frameId, "rejected", undefined, "component token lacks command scope");
227
+ // agent.shutdown converges on the shutdownAgent service (#342/#347): the #221 parent→child
228
+ // gate + canonical payload, identical to HTTP + MCP. The coarse command:write check above
229
+ // stays; the service adds the fine parent→child gate the bus path previously LACKED.
230
+ if (commandType === "agent.shutdown") {
231
+ if (conn.componentAuth && !isComponentAuthorizedFor(conn.componentAuth, { scope: "command:write", resource: commandAuthorizationResource({ target, params }) })) {
232
+ sendCommandResult(ws, frameId, "rejected", undefined, "component token lacks command scope");
233
+ return;
234
+ }
235
+ try {
236
+ const input = shutdownInputFromBusFrame(target, params, conn.agentId ?? conn.componentId);
237
+ const result = shutdownAgent(input, authContextFromBus(conn.componentAuth));
238
+ sendCommandResult(ws, frameId, "succeeded", { command: result.command });
239
+ } catch (e) {
240
+ if (e instanceof ShutdownAuthError) sendCommandResult(ws, frameId, "rejected", undefined, e.message);
241
+ else if (e instanceof ShutdownTargetError || e instanceof ValidationError) sendCommandResult(ws, frameId, "failed", undefined, e.message);
242
+ else throw e;
243
+ }
224
244
  return;
225
245
  }
226
246
 
227
- const command = createCommand({
228
- type: commandType,
229
- source: conn.agentId ?? conn.componentId,
230
- target,
231
- params,
232
- });
233
- emitCommandEvent(command, "command.requested");
234
- sendCommandResult(ws, frameId, "succeeded", { command });
247
+ // Dispatch through the shared service — it owns authorization (identical to the old
248
+ // busCommandAuthorized gate), createCommand, and the "command.requested" emit. The bus
249
+ // adapter only translates the typed ServiceAuthError into a rejected result frame.
250
+ try {
251
+ const command = dispatchCommand(
252
+ { type: commandType, source: conn.agentId ?? conn.componentId, target, params },
253
+ authContextFromBus(conn.componentAuth),
254
+ );
255
+ sendCommandResult(ws, frameId, "succeeded", { command });
256
+ } catch (e) {
257
+ if (e instanceof ServiceAuthError) {
258
+ sendCommandResult(ws, frameId, "rejected", undefined, "component token lacks command scope");
259
+ return;
260
+ }
261
+ throw e;
262
+ }
235
263
  }
236
264
 
237
265
  function busCommandAuthorized(
@@ -245,18 +273,6 @@ function busCommandAuthorized(
245
273
  });
246
274
  }
247
275
 
248
- function commandAuthorizationResource(command: Pick<Command, "target" | "params">) {
249
- const params = isRecord(command.params) ? command.params : {};
250
- return {
251
- target: command.target,
252
- agentId: stringValue(params.agentId) ?? command.target,
253
- policyName: stringValue(params.policyName),
254
- orchestratorId: stringValue(params.orchestratorId),
255
- cwd: stringValue(params.cwd),
256
- spawnRequestId: stringValue(params.spawnRequestId),
257
- };
258
- }
259
-
260
276
  function handleRegister(ws: BusWebSocket, frame: RegisterFrame): void {
261
277
  const payload = frame.payload;
262
278
  const runnerManaged = payload.meta?.runnerManaged === true || typeof payload.meta?.runnerId === "string";
@@ -273,10 +289,16 @@ function handleRegister(ws: BusWebSocket, frame: RegisterFrame): void {
273
289
  if (payload.agentId) {
274
290
  const label = stringMeta(payload.meta, "label");
275
291
  const context = contextFromMeta(payload.meta);
276
- const agent = upsertAgent({
292
+ // Thin transport: build the unified RegisterAgentInput from the wire frame and hand it to
293
+ // the registerAgent service. ALL registration side effects — authoritative parent lineage
294
+ // (resolved from the token constraints inside the service, never the wire body), the managed
295
+ // came-up-running reconcile, the status broadcast, the timeline note, the parent-wake (#351),
296
+ // and the audit row — live in src/services/register-agent.ts so this bus path can never drift
297
+ // from the HTTP path again (the #342 `spawned_by`-NULL class of bug).
298
+ const input: RegisterAgentInput = {
277
299
  id: payload.agentId,
278
300
  name: stringMeta(payload.meta, "name") ?? payload.componentId,
279
- kind: payload.role === "integration" ? "provider" : payload.role,
301
+ kind: (payload.role === "integration" ? "provider" : payload.role) as RegisterAgentInput["kind"],
280
302
  ...(label ? { label } : {}),
281
303
  tags: payload.tags,
282
304
  machine: payload.machine,
@@ -287,15 +309,9 @@ function handleRegister(ws: BusWebSocket, frame: RegisterFrame): void {
287
309
  ...(providerCapabilities ? { providerCapabilities } : {}),
288
310
  ...(context ? { context } : {}),
289
311
  meta: payload.meta,
290
- });
312
+ };
313
+ const agent = registerAgent(input, authContextFromBus(ws.data.componentAuth));
291
314
  epoch = agent.epoch;
292
- getLifecycleManager().onAgentRegistered(agent.id, {
293
- policyName: stringMeta(payload.meta, "policyName"),
294
- spawnRequestId: stringMeta(payload.meta, "spawnRequestId"),
295
- tmuxSession: stringMeta(payload.meta, "tmuxSession"),
296
- });
297
- noteAgentTimelineEvent(agent.id, agent.meta?.timelineEvent);
298
- emitAgentStatusEvent(agent.id);
299
315
  }
300
316
 
301
317
  busConnections.set(ws.data.id, {
@@ -151,6 +151,13 @@ export function getCompactionWatch(): CompactionWatch {
151
151
  return singleton;
152
152
  }
153
153
 
154
+ /** Test seam: stop and drop the singleton so each parity run starts with no
155
+ * armed watches. */
156
+ export function __resetCompactionWatch(): void {
157
+ singleton?.stop();
158
+ singleton = null;
159
+ }
160
+
154
161
  // Feed an agent's latched timelineEvent (from meta) to the watch. Called from
155
162
  // every path that updates an agent's meta — bus status/register frames and the
156
163
  // HTTP register route — so a real provider hook clears the watch regardless of
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import { gzipSync, brotliCompressSync, constants as zlibConstants } from "node:z
15
15
  import {
16
16
  VERSION,
17
17
  } from "./config";
18
+ import { CONTRACT_VERSIONS } from "./contracts";
18
19
  import {
19
20
  applyCors,
20
21
  assertSafeNetworkConfig,
@@ -38,12 +39,28 @@ async function main(): Promise<void> {
38
39
  startServer();
39
40
  }
40
41
 
41
- // On a version change, flag restart-on-update policies so they cycle when idle.
42
- function reconcileRelayVersion(): void {
43
- const stored = getConfig<string>("system", "relay-version")?.value;
44
- if (stored === VERSION) return;
45
- if (stored) getLifecycleManager().onRelayUpdated(VERSION);
46
- setConfig("system", "relay-version", VERSION, "server-startup");
42
+ // Restart-on-update gating (issue #341).
43
+ //
44
+ // A `restartOnUpdate` policy should cycle its managed agent only when the agent's
45
+ // running runner code is now protocol-INCOMPATIBLE with the relay — i.e. on a
46
+ // runner-protocol bump — NOT on every package version change. Gating on the package
47
+ // version churned the always-on Telegram agent ~12×/day (one restart per patch
48
+ // redeploy), dropping in-flight work (surfaced #340). The runner protocol
49
+ // (CONTRACT_VERSIONS.runnerProtocol) only moves on a breaking runner change, so this
50
+ // fires ~never during normal releases.
51
+ //
52
+ // `relay-version` is still tracked for observability/telemetry, but it no longer
53
+ // triggers restarts.
54
+ export function reconcileRelayVersion(): void {
55
+ const storedVersion = getConfig<string>("system", "relay-version")?.value;
56
+ if (storedVersion !== VERSION) setConfig("system", "relay-version", VERSION, "server-startup");
57
+
58
+ const storedProtocol = getConfig<string>("system", "runner-protocol-version")?.value;
59
+ const currentProtocol = String(CONTRACT_VERSIONS.runnerProtocol);
60
+ if (storedProtocol === currentProtocol) return;
61
+ // Skip the very first run (no stored protocol yet) — that's initial bootstrap, not an upgrade.
62
+ if (storedProtocol) getLifecycleManager().onRelayUpdated(VERSION);
63
+ setConfig("system", "runner-protocol-version", currentProtocol, "server-startup");
47
64
  }
48
65
 
49
66
  function startServer(): void {
@@ -1,6 +1,6 @@
1
1
  import { createCommand } from "./commands-db";
2
2
  import { isPathWithinBase } from "./utils";
3
- import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces, resolveQueuedPolicyMessages } from "./db";
3
+ import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces } from "./db";
4
4
  import {
5
5
  getManagedAgentState,
6
6
  listSpawnPolicies,
@@ -8,7 +8,8 @@ import {
8
8
  upsertManagedAgentState,
9
9
  } from "./config-store";
10
10
  import { emitRelayEvent } from "./events";
11
- import { emitMessageDeliveryUpdated } from "./sse";
11
+ import { markManagedAgentRunning, emitManagedState } from "./services/managed-running";
12
+ import { authContextFromSystem } from "./services/auth-context";
12
13
  import { emitCommandEvent } from "./command-events";
13
14
  import { buildManagedSpawnParams } from "./managed-policy";
14
15
  import { generateSpawnRequestId } from "./spawn-command";
@@ -75,50 +76,30 @@ export class LifecycleManager {
75
76
  this.spawnAgent(policy, "message-trigger");
76
77
  }
77
78
 
78
- onAgentRegistered(agentId: string, meta: { policyName?: string; spawnRequestId?: string; tmuxSession?: string | null }): void {
79
- if (!meta.policyName || !meta.spawnRequestId) return;
80
- const state = getManagedAgentState(meta.policyName);
81
- if (!state || state.spawnRequestId !== meta.spawnRequestId) return;
82
- const next = updateManagedAgentState(meta.policyName, {
83
- status: "running",
84
- agentId,
85
- tmuxSession: meta.tmuxSession ?? state.tmuxSession,
86
- healthySince: this.now(),
87
- backoffUntil: undefined,
88
- lastError: undefined,
89
- });
90
- if (next) this.emitState(next);
91
- const available = resolveQueuedPolicyMessages(meta.policyName, agentId);
92
- if (available.length) {
93
- emitRelayEvent({
94
- type: "message.available",
95
- source: "server",
96
- subject: `policy:${meta.policyName}`,
97
- data: { policyName: meta.policyName, agentId, messageIds: available.map((message) => message.id), count: available.length },
98
- });
99
- // queued → pending changed delivery_status; refresh the dashboard delivery
100
- // badge now rather than letting it sit stale until the next poll (#265).
101
- for (const message of available) emitMessageDeliveryUpdated(message);
102
- }
103
- }
79
+ // NOTE: the bus registration path used to call `onAgentRegistered` here for the
80
+ // came-up-running transition + queue flush. That logic now lives in the
81
+ // registerAgent service (src/services/register-agent.ts → markManagedAgentRunning),
82
+ // which the bus `handleRegister` calls directly — collapsing the second of the three
83
+ // drifted registration copies (epic #342). Only the orchestrator-report projection
84
+ // below still enters managed-running from the lifecycle manager.
104
85
 
105
86
  onOrchestratorManagedAgentsReported(orchestratorId: string, agents: ManagedAgent[], exitedAgents: ManagedSessionExitDiagnostics[] = []): void {
106
87
  for (const agent of agents) {
107
88
  if (!agent.policyName || !agent.spawnRequestId) continue;
108
89
  const state = getManagedAgentState(agent.policyName);
109
90
  if (!state || state.spawnRequestId !== agent.spawnRequestId) continue;
110
- const next = updateManagedAgentState(agent.policyName, {
111
- status: "running",
112
- agentId: agent.agentId || state.agentId,
113
- tmuxSession: agent.tmuxSession,
114
- workspaceId: agent.workspace?.id,
115
- workspacePath: agent.workspace?.worktreePath,
116
- workspaceBranch: agent.workspace?.branch,
117
- healthySince: state.healthySince ?? this.now(),
118
- backoffUntil: undefined,
119
- lastError: undefined,
120
- });
121
- if (next) this.emitState(next);
91
+ // Same came-up-running core the registration transports use — one home, no drift.
92
+ markManagedAgentRunning(
93
+ {
94
+ policyName: agent.policyName,
95
+ spawnRequestId: agent.spawnRequestId,
96
+ agentId: agent.agentId || state.agentId || "",
97
+ tmuxSession: agent.tmuxSession,
98
+ workspace: agent.workspace,
99
+ },
100
+ authContextFromSystem(),
101
+ { now: this.now },
102
+ );
122
103
 
123
104
  // Liveness reconciliation: the orchestrator just reported this process as
124
105
  // running. If the relay record says offline, the two truths disagree
@@ -511,40 +492,14 @@ export class LifecycleManager {
511
492
  context.utilization > DEFAULT_COMPACT_TARGET;
512
493
  }
513
494
 
495
+ // The managed-state emit + activity-transition record lives in the shared
496
+ // emitManagedState (src/services/managed-running.ts) so the registration service
497
+ // and this lifecycle reconciler emit identically (epic #342). We track the prior
498
+ // status per policy here only to compute the from→to for the transition row.
514
499
  private emitState(state: ManagedAgentState, reason?: string): void {
515
- emitRelayEvent({
516
- type: "policy.state.changed",
517
- source: "lifecycle-manager",
518
- subject: state.policyName,
519
- data: state as unknown as Record<string, unknown>,
520
- });
521
500
  const previous = this.lastStatusByPolicy.get(state.policyName);
522
501
  this.lastStatusByPolicy.set(state.policyName, state.status);
523
- if (previous !== state.status) this.recordTransition(state, previous, reason);
524
- }
525
-
526
- // Persist a managed-state transition so it shows on the activity feed and the
527
- // per-agent timeline — the "what changed, why, and when" record for a policy.
528
- private recordTransition(state: ManagedAgentState, fromState: string | undefined, reason?: string): void {
529
- const why = reason ?? state.lastError ?? undefined;
530
- const seq = ++this.transitionSeq;
531
- createActivityEvent({
532
- clientId: `lifecycle-${state.policyName}-${state.status}-${this.now()}-${seq}`,
533
- kind: "state",
534
- title: `${state.policyName}: ${fromState ?? "—"} → ${state.status}`,
535
- body: why,
536
- icon: "ti-arrows-exchange",
537
- view: "managed-agents",
538
- agentId: state.agentId,
539
- metadata: {
540
- source: "lifecycle-manager",
541
- policyName: state.policyName,
542
- spawnRequestId: state.spawnRequestId,
543
- fromState: fromState ?? null,
544
- toState: state.status,
545
- reason: why ?? null,
546
- },
547
- });
502
+ emitManagedState(state, previous, reason, this.now);
548
503
  }
549
504
 
550
505
  // Surface a disagreement between relay status and real process liveness so the
@@ -624,6 +579,13 @@ export function getLifecycleManager(): LifecycleManager {
624
579
  return singleton;
625
580
  }
626
581
 
582
+ /** Test seam: stop and drop the singleton so each parity run starts with clean
583
+ * in-memory state (the per-policy last-status map, timers). */
584
+ export function __resetLifecycleManager(): void {
585
+ singleton?.stop();
586
+ singleton = null;
587
+ }
588
+
627
589
  function alwaysReloadTags(tags: string[]): string[] {
628
590
  return tags
629
591
  .filter((tag) => tag.startsWith("memory-reload:"))