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.
- package/docs/openapi.json +1 -1
- package/package.json +1 -1
- package/public/assets/{activity-C3mkM6AU.js → activity-BgkmA1lh.js} +2 -2
- package/public/assets/{activity-C3mkM6AU.js.map → activity-BgkmA1lh.js.map} +1 -1
- package/public/assets/{agents-CAhQO7JH.js → agents-B7HnuAXx.js} +2 -2
- package/public/assets/{agents-CAhQO7JH.js.map → agents-B7HnuAXx.js.map} +1 -1
- package/public/assets/{analytics-BwihhhNn.js → analytics-0-akxJCJ.js} +2 -2
- package/public/assets/{analytics-BwihhhNn.js.map → analytics-0-akxJCJ.js.map} +1 -1
- package/public/assets/{automation-BLXToUiU.js → automation-CaE1z_-M.js} +2 -2
- package/public/assets/{automation-BLXToUiU.js.map → automation-CaE1z_-M.js.map} +1 -1
- package/public/assets/{chat-8iIPyww9.js → chat-BANKUW05.js} +2 -2
- package/public/assets/{chat-8iIPyww9.js.map → chat-BANKUW05.js.map} +1 -1
- package/public/assets/{formatted-body-impl-BHH0wzY7.js → formatted-body-impl-DExNPNsL.js} +2 -2
- package/public/assets/{formatted-body-impl-BHH0wzY7.js.map → formatted-body-impl-DExNPNsL.js.map} +1 -1
- package/public/assets/{index-CaauKXl9.js → index-DEZdON6c.js} +5 -5
- package/public/assets/{index-CaauKXl9.js.map → index-DEZdON6c.js.map} +1 -1
- package/public/assets/{maintenance-9n_rJCHT.js → maintenance-DpTdJxQp.js} +2 -2
- package/public/assets/{maintenance-9n_rJCHT.js.map → maintenance-DpTdJxQp.js.map} +1 -1
- package/public/assets/{managed-agents-Rp2-xpBx.js → managed-agents-B3df2xfk.js} +2 -2
- package/public/assets/{managed-agents-Rp2-xpBx.js.map → managed-agents-B3df2xfk.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-YfJsGh6I.js → markdown-preview-impl-D0Zj7c3T.js} +2 -2
- package/public/assets/{markdown-preview-impl-YfJsGh6I.js.map → markdown-preview-impl-D0Zj7c3T.js.map} +1 -1
- package/public/assets/{memory-BQONtGQS.js → memory-TATN2vZf.js} +2 -2
- package/public/assets/{memory-BQONtGQS.js.map → memory-TATN2vZf.js.map} +1 -1
- package/public/assets/{messages-DGqpkH72.js → messages-3rS1lxIf.js} +2 -2
- package/public/assets/{messages-DGqpkH72.js.map → messages-3rS1lxIf.js.map} +1 -1
- package/public/assets/{orchestrators-b8k9QoGv.js → orchestrators-CRIV0g5y.js} +2 -2
- package/public/assets/{orchestrators-b8k9QoGv.js.map → orchestrators-CRIV0g5y.js.map} +1 -1
- package/public/assets/{overview-DSU_CggA.js → overview-CmHC_5oM.js} +2 -2
- package/public/assets/{overview-DSU_CggA.js.map → overview-CmHC_5oM.js.map} +1 -1
- package/public/assets/{pairs-DGocNC1U.js → pairs-DBPAhXTI.js} +2 -2
- package/public/assets/{pairs-DGocNC1U.js.map → pairs-DBPAhXTI.js.map} +1 -1
- package/public/assets/{security-BSh0QxOl.js → security-572X5MNX.js} +2 -2
- package/public/assets/{security-BSh0QxOl.js.map → security-572X5MNX.js.map} +1 -1
- package/public/assets/{settings-C03CAJgO.js → settings-vTBu8w3O.js} +2 -2
- package/public/assets/{settings-C03CAJgO.js.map → settings-vTBu8w3O.js.map} +1 -1
- package/public/assets/{tasks-rKbuUPOk.js → tasks-C0bPrDgN.js} +2 -2
- package/public/assets/{tasks-rKbuUPOk.js.map → tasks-C0bPrDgN.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CA8u4jh3.js → terminal-viewer-impl-rVPA6Fsx.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CA8u4jh3.js.map → terminal-viewer-impl-rVPA6Fsx.js.map} +1 -1
- package/public/assets/{work-queue-DOsA9s4M.js → work-queue-BxkpTt_A.js} +2 -2
- package/public/assets/{work-queue-DOsA9s4M.js.map → work-queue-BxkpTt_A.js.map} +1 -1
- package/public/index.html +1 -1
- package/runner/src/adapter.ts +7 -0
- package/scripts/orchestrator-spawn-smoke.ts +65 -33
- package/src/agent-ref.ts +28 -1
- package/src/bus.ts +51 -35
- package/src/compaction-watch.ts +7 -0
- package/src/index.ts +23 -6
- package/src/lifecycle-manager.ts +33 -71
- package/src/mcp.ts +100 -308
- package/src/routes/agent-sessions.ts +38 -3
- package/src/routes/agents-spawn.ts +43 -175
- package/src/routes/commands.ts +7 -19
- package/src/routes/messages.ts +24 -87
- package/src/security.ts +15 -0
- package/src/services/auth-context.ts +109 -0
- package/src/services/dispatch-command.ts +60 -0
- package/src/services/errors.ts +26 -0
- package/src/services/managed-running.ts +130 -0
- package/src/services/parity-harness.ts +135 -0
- package/src/services/register-agent.ts +74 -0
- package/src/services/send-message.ts +159 -0
- package/src/services/shutdown-agent.ts +234 -0
- 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-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
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-
|
|
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">
|
package/runner/src/adapter.ts
CHANGED
|
@@ -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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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, {
|
package/src/compaction-watch.ts
CHANGED
|
@@ -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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 {
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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:"))
|