agent-relay-server 0.91.3 → 0.92.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 +2 -2
- package/public/assets/MessageBubble-CUddRP1E.js +3 -0
- package/public/assets/{MessageBubble-Cl3gC5sO.js.map → MessageBubble-CUddRP1E.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BAu-lTm9.js → PlanGraphPanel-CpIBMvnu.js} +2 -2
- package/public/assets/{PlanGraphPanel-BAu-lTm9.js.map → PlanGraphPanel-CpIBMvnu.js.map} +1 -1
- package/public/assets/{activity-navjipA_.js → activity-B6WPWNsX.js} +2 -2
- package/public/assets/{activity-navjipA_.js.map → activity-B6WPWNsX.js.map} +1 -1
- package/public/assets/{agents-B09rJRik.js → agents-BebF-yQr.js} +2 -2
- package/public/assets/{agents-B09rJRik.js.map → agents-BebF-yQr.js.map} +1 -1
- package/public/assets/{automation-BsXT9mKj.js → automation-CyPXC1A8.js} +2 -2
- package/public/assets/{automation-BsXT9mKj.js.map → automation-CyPXC1A8.js.map} +1 -1
- package/public/assets/chat-DfqC3kFF.js +2 -0
- package/public/assets/{chat-Cmv-YVII.js.map → chat-DfqC3kFF.js.map} +1 -1
- package/public/assets/display-CWMHll1J.js.map +1 -1
- package/public/assets/{formatted-body-impl-CwziXint.js → formatted-body-impl-CrE-hFvy.js} +2 -2
- package/public/assets/{formatted-body-impl-CwziXint.js.map → formatted-body-impl-CrE-hFvy.js.map} +1 -1
- package/public/assets/{index-Dm1zlklN.js → index-CdDX5NR1.js} +5 -5
- package/public/assets/{index-Dm1zlklN.js.map → index-CdDX5NR1.js.map} +1 -1
- package/public/assets/index-umexz3cs.css +2 -0
- package/public/assets/{insights-CGH0lI9i.js → insights-BribQHXe.js} +2 -2
- package/public/assets/{insights-CGH0lI9i.js.map → insights-BribQHXe.js.map} +1 -1
- package/public/assets/{maintenance-BhpfPv6u.js → maintenance-Cjol3fS_.js} +2 -2
- package/public/assets/{maintenance-BhpfPv6u.js.map → maintenance-Cjol3fS_.js.map} +1 -1
- package/public/assets/{managed-agents-CwjcuvR6.js → managed-agents-1lZwQNHl.js} +2 -2
- package/public/assets/{managed-agents-CwjcuvR6.js.map → managed-agents-1lZwQNHl.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-KBRWvxHt.js → markdown-preview-impl-C2WeUNFR.js} +2 -2
- package/public/assets/{markdown-preview-impl-KBRWvxHt.js.map → markdown-preview-impl-C2WeUNFR.js.map} +1 -1
- package/public/assets/{memory-CgpkkEKK.js → memory-B7mcELEW.js} +2 -2
- package/public/assets/{memory-CgpkkEKK.js.map → memory-B7mcELEW.js.map} +1 -1
- package/public/assets/{messages-BV1z5tMO.js → messages-B8jF9p2l.js} +2 -2
- package/public/assets/{messages-BV1z5tMO.js.map → messages-B8jF9p2l.js.map} +1 -1
- package/public/assets/{orchestrators-Bv6T_n-l.js → orchestrators-CQjvBXDx.js} +2 -2
- package/public/assets/{orchestrators-Bv6T_n-l.js.map → orchestrators-CQjvBXDx.js.map} +1 -1
- package/public/assets/{overview-BdErDjo5.js → overview-CgDUJOG-.js} +2 -2
- package/public/assets/{overview-BdErDjo5.js.map → overview-CgDUJOG-.js.map} +1 -1
- package/public/assets/{pairs-BpzbJ5OL.js → pairs-BdPKQyzE.js} +2 -2
- package/public/assets/{pairs-BpzbJ5OL.js.map → pairs-BdPKQyzE.js.map} +1 -1
- package/public/assets/{security-CRsIxNpi.js → security-BlZQ0ECI.js} +2 -2
- package/public/assets/{security-CRsIxNpi.js.map → security-BlZQ0ECI.js.map} +1 -1
- package/public/assets/{select-DmZ1eEzM.js → select-CjpI0JtN.js} +2 -2
- package/public/assets/{select-DmZ1eEzM.js.map → select-CjpI0JtN.js.map} +1 -1
- package/public/assets/{settings-BqmYUpOu.js → settings-CMgqpUY-.js} +2 -2
- package/public/assets/{settings-BqmYUpOu.js.map → settings-CMgqpUY-.js.map} +1 -1
- package/public/assets/{tasks-DZ8C8K2N.js → tasks-BcHWptn4.js} +2 -2
- package/public/assets/{tasks-DZ8C8K2N.js.map → tasks-BcHWptn4.js.map} +1 -1
- package/public/assets/{teams-VmX_6z_9.js → teams-BGL-VpqZ.js} +2 -2
- package/public/assets/{teams-VmX_6z_9.js.map → teams-BGL-VpqZ.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BJilWDSR.js → terminal-viewer-impl-D8bnx7Z7.js} +2 -2
- package/public/assets/{terminal-viewer-impl-BJilWDSR.js.map → terminal-viewer-impl-D8bnx7Z7.js.map} +1 -1
- package/public/assets/{user-avatar-jqI4eKDf.js → user-avatar-DZ1g4X7l.js} +2 -2
- package/public/assets/{user-avatar-jqI4eKDf.js.map → user-avatar-DZ1g4X7l.js.map} +1 -1
- package/public/assets/{work-queue-lFDBNpds.js → work-queue-CIy6fe6y.js} +2 -2
- package/public/assets/{work-queue-lFDBNpds.js.map → work-queue-CIy6fe6y.js.map} +1 -1
- package/public/index.html +2 -2
- package/src/automations/dispatch.ts +3 -1
- package/src/bus.ts +9 -2
- package/src/config-store.ts +24 -0
- package/src/config.ts +1 -0
- package/src/db/agent-predicates.ts +21 -0
- package/src/db/agents.ts +3 -3
- package/src/db/delivery.ts +38 -0
- package/src/db/messages.ts +18 -1
- package/src/db/workspaces.ts +44 -2
- package/src/maintenance/constants.ts +2 -0
- package/src/maintenance/jobs.ts +21 -8
- package/src/maintenance/workspaces.ts +76 -0
- package/src/managed-policy.ts +3 -1
- package/src/mcp/tools-spawn.ts +43 -3
- package/src/project-settings.ts +27 -0
- package/src/project-skills.ts +27 -0
- package/src/routes/agent-sessions.ts +3 -1
- package/src/routes/agents-spawn.ts +19 -2
- package/src/routes/agents.ts +17 -2
- package/src/routes/commands.ts +5 -0
- package/src/services/managed-running.ts +18 -1
- package/src/services/rate-limit-resume.ts +3 -0
- package/src/services/register-agent.ts +7 -1
- package/src/services/spawn-agent.ts +75 -11
- package/src/shared-mcp.ts +46 -17
- package/src/spawn-command.ts +10 -0
- package/src/validation.ts +7 -2
- package/src/workspace-auto-merge.ts +53 -9
- package/public/assets/MessageBubble-Cl3gC5sO.js +0 -3
- package/public/assets/chat-Cmv-YVII.js +0 -2
- package/public/assets/index-F-FSPv4X.css +0 -2
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{w as s}from"./index-
|
|
2
|
-
//# sourceMappingURL=user-avatar-
|
|
1
|
+
import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{w as s}from"./index-CdDX5NR1.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
|
|
2
|
+
//# sourceMappingURL=user-avatar-DZ1g4X7l.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-avatar-
|
|
1
|
+
{"version":3,"file":"user-avatar-DZ1g4X7l.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DsqUEZOf.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{V as f,q as p,s as m}from"./index-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
1
|
+
import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DsqUEZOf.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{V as f,q as p,s as m}from"./index-CdDX5NR1.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:i,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
|
|
2
|
+
//# sourceMappingURL=work-queue-CIy6fe6y.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-queue-lFDBNpds.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-CIy6fe6y.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-CdDX5NR1.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-CP_edrrL.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/lucide-react-CsPXvXMC.js">
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<link rel="modulepreload" crossorigin href="./assets/badge-CK6ejiOh.js">
|
|
38
38
|
<link rel="modulepreload" crossorigin href="./assets/input-CLW2gDUM.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="./assets/themes-mW9zZP4O.js">
|
|
40
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
40
|
+
<link rel="stylesheet" crossorigin href="./assets/index-umexz3cs.css">
|
|
41
41
|
</head>
|
|
42
42
|
<body class="bg-background text-foreground antialiased">
|
|
43
43
|
<div id="root"></div>
|
|
@@ -3,6 +3,7 @@ import { createCommand } from "../commands-db";
|
|
|
3
3
|
import { getAgentProfile, getSpawnPolicy } from "../config-store";
|
|
4
4
|
import { getOrchestrator, ingestIntegrationEvent, listAgents, ValidationError, withResolvedProvisioning } from "../db";
|
|
5
5
|
import { runnerRuntimeTokenEnv } from "../runtime-tokens";
|
|
6
|
+
import { applyDefaultSharedMcp } from "../shared-mcp";
|
|
6
7
|
import { buildSpawnCommand, resolveSpawnModelParams } from "../spawn-command";
|
|
7
8
|
import { nextScheduledAt, formatRunDate } from "./cron";
|
|
8
9
|
import {
|
|
@@ -89,7 +90,8 @@ function dispatchOnDemandAutomation(
|
|
|
89
90
|
now: number,
|
|
90
91
|
): AutomationDispatchResult {
|
|
91
92
|
if (!orchestrator.providers.includes(policy.provider)) throw new ValidationError(`orchestrator ${orchestrator.id} does not have provider available: ${policy.provider}`);
|
|
92
|
-
const
|
|
93
|
+
const resolvedProfile = withResolvedProvisioning(policy.profile ? getAgentProfile(policy.profile)?.value : undefined, policy.provider);
|
|
94
|
+
const agentProfile = resolvedProfile ? applyDefaultSharedMcp(resolvedProfile) : undefined;
|
|
93
95
|
const label = automationRunLabel(automation.id, run.id);
|
|
94
96
|
const command = createCommand({
|
|
95
97
|
type: "agent.spawn",
|
package/src/bus.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { projectAgentStatusData } from "./agent-card-projection";
|
|
|
5
5
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
6
6
|
import { getCommand, updateCommand } from "./commands-db";
|
|
7
7
|
import { emitCommandEvent } from "./command-events";
|
|
8
|
-
import { getLifecycleManager } from "./lifecycle-manager";
|
|
9
8
|
import { noteAgentTimelineEvent, noteCompactionCommandCompleted } from "./compaction-watch";
|
|
10
9
|
import { reconcileTerminalProviderExit } from "./services/terminal-provider-exit";
|
|
11
10
|
import { registerAgent } from "./services/register-agent";
|
|
@@ -14,6 +13,8 @@ import { commandAuthorizationResource, dispatchCommand } from "./services/dispat
|
|
|
14
13
|
import { auditProviderStateTransition } from "./services/provider-state-audit";
|
|
15
14
|
import { ServiceAuthError } from "./services/errors";
|
|
16
15
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
|
|
16
|
+
import { flushQueuedDirectMessages } from "./services/managed-running";
|
|
17
|
+
import { isAgentUnavailable } from "./db/agent-predicates";
|
|
17
18
|
import { applyCommandToRecipe } from "./recipe-runner";
|
|
18
19
|
import { enqueueContinuationInjectionAfterSelfResume } from "./self-resume";
|
|
19
20
|
import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
|
|
@@ -121,7 +122,6 @@ export function expireStaleBusAgents(graceMs = busStaleGraceMs()): { agentIds: s
|
|
|
121
122
|
revokeRuntimeTokensForAgent(row.id, Date.now(), "stale");
|
|
122
123
|
setStatus(row.id, "offline");
|
|
123
124
|
emitAgentStatusEvent(row.id);
|
|
124
|
-
getLifecycleManager().onAgentDisappeared(row.id);
|
|
125
125
|
const tasks = orphanTasksForAgent(row.id);
|
|
126
126
|
for (const task of tasks) {
|
|
127
127
|
orphanedTasks.push(task);
|
|
@@ -152,10 +152,16 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
|
|
|
152
152
|
const current = getAgent(conn.agentId);
|
|
153
153
|
const heldOffline = current?.status === "offline" && isRecord(current.meta?.terminalProviderExit);
|
|
154
154
|
if (!heldOffline) {
|
|
155
|
+
const wasUnavailable = current ? isAgentUnavailable(current) : false;
|
|
155
156
|
const runtime = runtimeFromMeta(frame.payload.meta);
|
|
156
157
|
heartbeat(conn.agentId, { instanceId: conn.instanceId, epoch: conn.epoch }, runtime);
|
|
157
158
|
if (frame.payload.status) setStatus(conn.agentId, frame.payload.status, { instanceId: conn.instanceId, epoch: conn.epoch });
|
|
158
159
|
emitAgentStatusEvent(conn.agentId);
|
|
160
|
+
// #416 — flush queued messages if the agent transitioned from unavailable to available.
|
|
161
|
+
if (wasUnavailable) {
|
|
162
|
+
const after = getAgent(conn.agentId);
|
|
163
|
+
if (after && !isAgentUnavailable(after)) flushQueuedDirectMessages(conn.agentId);
|
|
164
|
+
}
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
if (frame.id) send(ws, { type: "ack", payload: { frameId: frame.id } });
|
|
@@ -652,6 +658,7 @@ function isContextLifecycleState(value: unknown): value is ContextState["lifecyc
|
|
|
652
658
|
value === "working" ||
|
|
653
659
|
value === "cooling" ||
|
|
654
660
|
value === "compacting" ||
|
|
661
|
+
value === "clearing-context" ||
|
|
655
662
|
value === "hibernating";
|
|
656
663
|
}
|
|
657
664
|
|
package/src/config-store.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-res
|
|
|
6
6
|
import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
|
|
7
7
|
import { PROJECT_INSTRUCTION_POLICY_NAMES } from "./project-instructions";
|
|
8
8
|
import { PROJECT_MCP_POLICY_NAMES } from "./project-mcp";
|
|
9
|
+
import { PROJECT_SETTINGS_POLICY_NAMES } from "./project-settings";
|
|
10
|
+
import { PROJECT_SKILLS_POLICY_NAMES } from "./project-skills";
|
|
9
11
|
import { SHARED_MCP_POLICY_NAMES } from "./shared-mcp";
|
|
10
12
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
11
13
|
import { errMessage, isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES } from "agent-relay-sdk"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
@@ -238,6 +240,24 @@ function cleanAgentProfileProjectMcp(value: unknown): AgentProfile["projectMcp"]
|
|
|
238
240
|
return { policy: cleanEnum(policy, "projectMcp.policy", PROJECT_MCP_POLICY_NAMES) as NonNullable<AgentProfile["projectMcp"]>["policy"] };
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
function cleanAgentProfileProjectSkills(value: unknown): AgentProfile["projectSkills"] {
|
|
244
|
+
if (value === undefined || value === null || value === false) return undefined;
|
|
245
|
+
if (value === true) return {};
|
|
246
|
+
if (!isRecord(value)) throw new ValidationError("projectSkills must be a boolean or { policy } object");
|
|
247
|
+
const policy = value.policy;
|
|
248
|
+
if (policy === undefined || policy === null) return {};
|
|
249
|
+
return { policy: cleanEnum(policy, "projectSkills.policy", PROJECT_SKILLS_POLICY_NAMES) as NonNullable<AgentProfile["projectSkills"]>["policy"] };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function cleanAgentProfileProjectSettings(value: unknown): AgentProfile["projectSettings"] {
|
|
253
|
+
if (value === undefined || value === null || value === false) return undefined;
|
|
254
|
+
if (value === true) return {};
|
|
255
|
+
if (!isRecord(value)) throw new ValidationError("projectSettings must be a boolean or { policy } object");
|
|
256
|
+
const policy = value.policy;
|
|
257
|
+
if (policy === undefined || policy === null) return {};
|
|
258
|
+
return { policy: cleanEnum(policy, "projectSettings.policy", PROJECT_SETTINGS_POLICY_NAMES) as NonNullable<AgentProfile["projectSettings"]>["policy"] };
|
|
259
|
+
}
|
|
260
|
+
|
|
241
261
|
// #672 — shared host-listener MCP projection profile default. Mirrors projectMcp:
|
|
242
262
|
// boolean | { policy } → normalized { policy? } | undefined (OFF).
|
|
243
263
|
function cleanAgentProfileSharedMcp(value: unknown): AgentProfile["sharedMcp"] {
|
|
@@ -270,6 +290,8 @@ function agentProfileDefaults(input: Pick<AgentProfile, "name" | "base"> & Parti
|
|
|
270
290
|
},
|
|
271
291
|
...(input.projectInstructions ? { projectInstructions: input.projectInstructions } : {}),
|
|
272
292
|
...(input.projectMcp ? { projectMcp: input.projectMcp } : {}),
|
|
293
|
+
...(input.projectSkills ? { projectSkills: input.projectSkills } : {}),
|
|
294
|
+
...(input.projectSettings ? { projectSettings: input.projectSettings } : {}),
|
|
273
295
|
...(input.sharedMcp ? { sharedMcp: input.sharedMcp } : {}),
|
|
274
296
|
relay: {
|
|
275
297
|
context: input.relay?.context ?? !isolated,
|
|
@@ -346,6 +368,8 @@ function validateAgentProfile(key: string, value: unknown): AgentProfile {
|
|
|
346
368
|
},
|
|
347
369
|
projectInstructions: cleanAgentProfileProjectInstructions(value.projectInstructions),
|
|
348
370
|
projectMcp: cleanAgentProfileProjectMcp(value.projectMcp),
|
|
371
|
+
projectSkills: cleanAgentProfileProjectSkills(value.projectSkills),
|
|
372
|
+
projectSettings: cleanAgentProfileProjectSettings(value.projectSettings),
|
|
349
373
|
sharedMcp: cleanAgentProfileSharedMcp(value.sharedMcp),
|
|
350
374
|
relay: {
|
|
351
375
|
context: relay.context === undefined ? defaults.relay.context : cleanBoolean(relay.context, "relay.context"),
|
package/src/config.ts
CHANGED
|
@@ -154,6 +154,7 @@ export const WORKSPACE_REVIEW_TTL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE
|
|
|
154
154
|
export const WORKSPACE_GC_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_GC_INTERVAL_MS", 60 * 60 * 1000);
|
|
155
155
|
export const WORKSPACE_ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_ORPHAN_REAPER_INTERVAL_MS", 30 * 60 * 1000);
|
|
156
156
|
export const WORKSPACE_AUTO_MERGE_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_AUTO_MERGE_INTERVAL_MS", CONFLICT_SCAN_INTERVAL_MS);
|
|
157
|
+
export const WORKSPACE_IDLE_REFRESH_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_IDLE_REFRESH_INTERVAL_MS", CONFLICT_SCAN_INTERVAL_MS);
|
|
157
158
|
export const ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_REAPER_INTERVAL_MS", 5 * 60 * 1000);
|
|
158
159
|
export const STEWARD_WAKE_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_STEWARD_WAKE_COOLDOWN_MS", 10 * 60 * 1000);
|
|
159
160
|
export const UNLANDED_FLAG_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_FLAG_COOLDOWN_MS", 6 * 60 * 60 * 1000);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isProviderBlocked } from "../provider-state.ts";
|
|
1
2
|
import type { AgentCard } from "../types";
|
|
2
3
|
|
|
3
4
|
export function isDeliveryAgent(agent: AgentCard): boolean {
|
|
@@ -7,3 +8,23 @@ export function isDeliveryAgent(agent: AgentCard): boolean {
|
|
|
7
8
|
agent.kind !== "channel" &&
|
|
8
9
|
agent.meta?.kind !== "channel";
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True when the agent is registered and known to the relay but is currently in a state
|
|
14
|
+
* where it cannot immediately process new messages: stale (missed heartbeats), provider-
|
|
15
|
+
* blocked (rate-limited, API error hold), compacting/clearing context, or registered but
|
|
16
|
+
* not yet ready (starting up).
|
|
17
|
+
*
|
|
18
|
+
* Messages sent to an unavailable agent are stored as `delivery_status = 'queued'` and
|
|
19
|
+
* flushed when the agent transitions back to available (heartbeat, re-registration, or
|
|
20
|
+
* rate-limit-resume). This is distinct from `status === "offline"` which uses the existing
|
|
21
|
+
* store-ahead path for unknown/deleted targets.
|
|
22
|
+
*/
|
|
23
|
+
export function isAgentUnavailable(agent: AgentCard): boolean {
|
|
24
|
+
if (agent.status === "stale") return true;
|
|
25
|
+
if (isProviderBlocked(agent)) return true;
|
|
26
|
+
const lcs = agent.context?.lifecycleState;
|
|
27
|
+
if (lcs === "compacting" || lcs === "clearing-context") return true;
|
|
28
|
+
if (!agent.ready && agent.status !== "offline") return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
package/src/db/agents.ts
CHANGED
|
@@ -395,7 +395,7 @@ export function heartbeat(
|
|
|
395
395
|
.query(`
|
|
396
396
|
UPDATE agents SET
|
|
397
397
|
last_seen = ?,
|
|
398
|
-
status = CASE WHEN status
|
|
398
|
+
status = CASE WHEN status IN ('offline', 'stale') THEN 'idle' ELSE status END,
|
|
399
399
|
provider_capabilities = coalesce(?, provider_capabilities),
|
|
400
400
|
context_state = coalesce(?, context_state),
|
|
401
401
|
quota_state = coalesce(?, quota_state)
|
|
@@ -412,7 +412,7 @@ export function heartbeat(
|
|
|
412
412
|
return result.changes > 0;
|
|
413
413
|
}
|
|
414
414
|
const result = getDb()
|
|
415
|
-
.query("UPDATE agents SET last_seen = ?, status = CASE WHEN status
|
|
415
|
+
.query("UPDATE agents SET last_seen = ?, status = CASE WHEN status IN ('offline', 'stale') THEN 'idle' ELSE status END WHERE id = ?")
|
|
416
416
|
.run(now, id);
|
|
417
417
|
return result.changes > 0;
|
|
418
418
|
}
|
|
@@ -495,7 +495,7 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
|
|
|
495
495
|
getDb().query("UPDATE agents SET last_seen = ? WHERE id IN ('user', 'system')").run(now);
|
|
496
496
|
const rows = getDb()
|
|
497
497
|
.query(
|
|
498
|
-
"UPDATE agents SET status = '
|
|
498
|
+
"UPDATE agents SET status = 'stale', ready = 0 WHERE status NOT IN ('offline', 'stale') AND last_seen < ? AND id NOT IN ('user', 'system') RETURNING id"
|
|
499
499
|
)
|
|
500
500
|
.all(cutoff) as any[];
|
|
501
501
|
for (const row of rows) {
|
package/src/db/delivery.ts
CHANGED
|
@@ -413,6 +413,44 @@ export function resolveQueuedPolicyMessages(policyName: string, agentId: string)
|
|
|
413
413
|
return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Flush queued messages addressed directly to `agentId` (not via a policy target): transition
|
|
418
|
+
* their `delivery_status` from `queued` to `pending` and stamp `resolved_to_agent` so the agent
|
|
419
|
+
* picks them up on its next poll. Ordered by `queued_at ASC, id ASC` (FIFO). Parallel to
|
|
420
|
+
* `resolveQueuedPolicyMessages`, which handles `policy:xxx` targets.
|
|
421
|
+
*
|
|
422
|
+
* Returns the flushed messages so callers can emit `message.available` / `message.new` /
|
|
423
|
+
* `message.delivery_updated` events.
|
|
424
|
+
*/
|
|
425
|
+
export function resolveQueuedDirectMessages(agentId: string): Message[] {
|
|
426
|
+
const rows = getDb().query(`
|
|
427
|
+
SELECT m.id
|
|
428
|
+
FROM messages m
|
|
429
|
+
WHERE m.to_target = ?
|
|
430
|
+
AND m.delivery_status = 'queued'
|
|
431
|
+
ORDER BY COALESCE(m.queued_at, m.created_at) ASC, m.id ASC
|
|
432
|
+
`).all(agentId) as Array<{ id: number }>;
|
|
433
|
+
if (rows.length === 0) return [];
|
|
434
|
+
const ids = rows.map((row) => row.id);
|
|
435
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
getDb().query(`
|
|
438
|
+
UPDATE messages
|
|
439
|
+
SET delivery_status = 'pending',
|
|
440
|
+
resolved_to_agent = ?,
|
|
441
|
+
delivery_attempts = delivery_attempts + 1,
|
|
442
|
+
delivery_last_error = NULL,
|
|
443
|
+
delivery_next_retry_at = NULL,
|
|
444
|
+
delivery_poison_reason = NULL,
|
|
445
|
+
delivery_updated_at = ?
|
|
446
|
+
WHERE id IN (${placeholders})
|
|
447
|
+
`).run(agentId, now, ...ids);
|
|
448
|
+
for (const id of ids) {
|
|
449
|
+
insertMessageDeliveryAttempt(id, { agentId, status: "pending" }, now);
|
|
450
|
+
}
|
|
451
|
+
return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
|
|
452
|
+
}
|
|
453
|
+
|
|
416
454
|
export function expireQueuedMessages(now: number = Date.now()): Message[] {
|
|
417
455
|
const rows = getDb().query(`
|
|
418
456
|
SELECT id FROM messages
|
package/src/db/messages.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { getAgent, validateAgentSession } from "./agents.ts";
|
|
|
19
19
|
import { cleanAttachmentRefs, linkAttachmentRefs, validateAttachmentRefs } from "./artifacts.ts";
|
|
20
20
|
import { ClaimError, ValidationError, getDb } from "./connection.ts";
|
|
21
21
|
import { enforceQueueLimit, isChannelAgentId, matchingDeliveryAgents } from "./delivery.ts";
|
|
22
|
+
import { isAgentUnavailable } from "./agent-predicates.ts";
|
|
22
23
|
import { MSG_SELECT, normalizeReactionEmoji, rowToMessage } from "./mappers.ts";
|
|
23
24
|
import { claimMessageRow } from "./message-claims.ts";
|
|
24
25
|
import { getMessage } from "./message-queries.ts";
|
|
@@ -469,6 +470,19 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
469
470
|
queuedAt = now;
|
|
470
471
|
maxAgeSeconds = maxAgeSeconds ?? 86_400;
|
|
471
472
|
}
|
|
473
|
+
} else if (!isFanoutTarget(input.to) && !isMechanicalMessageKind(input.kind)) {
|
|
474
|
+
// #416 — queue messages to a registered-but-temporarily-unavailable direct target so they
|
|
475
|
+
// are not silently held as "pending" while the agent cannot process them. The flush fires
|
|
476
|
+
// when the agent returns to available (heartbeat stale→idle, re-registration, rate-limit
|
|
477
|
+
// resume). Offline agents use the existing store-ahead path (policyName is null, status
|
|
478
|
+
// "offline" → resolveQueuedDirectMessages is never called for them — the queue is cleared
|
|
479
|
+
// only when the agent actually comes back as non-offline).
|
|
480
|
+
const directAgent = getAgent(input.to);
|
|
481
|
+
if (directAgent && isAgentUnavailable(directAgent)) {
|
|
482
|
+
deliveryStatus = "queued";
|
|
483
|
+
queuedAt = now;
|
|
484
|
+
maxAgeSeconds = maxAgeSeconds ?? 86_400;
|
|
485
|
+
}
|
|
472
486
|
}
|
|
473
487
|
|
|
474
488
|
// #451/#461 — moderate default TTL for non-claimable FAN-OUT messages the sender didn't bound,
|
|
@@ -509,7 +523,10 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
509
523
|
const newId = Number(result.lastInsertRowid);
|
|
510
524
|
if (threadId === null) setSelfThread.run(newId, newId);
|
|
511
525
|
linkAttachmentRefs("message", newId, attachmentRefs, input.from);
|
|
512
|
-
if (
|
|
526
|
+
if (deliveryStatus === "queued") {
|
|
527
|
+
if (policyName) enforceQueueLimit(`policy:${policyName}`);
|
|
528
|
+
else enforceQueueLimit(input.to);
|
|
529
|
+
}
|
|
513
530
|
return newId;
|
|
514
531
|
})();
|
|
515
532
|
|
package/src/db/workspaces.ts
CHANGED
|
@@ -74,10 +74,15 @@ import type {
|
|
|
74
74
|
ProviderCapabilities,
|
|
75
75
|
TaskStatusInput,
|
|
76
76
|
WorkspaceMetadata,
|
|
77
|
+
WorkspaceAutoMergePolicy,
|
|
78
|
+
WorkspaceLandingPolicy,
|
|
77
79
|
WorkspaceRecord,
|
|
78
80
|
WorkspaceStatus,
|
|
79
81
|
} from "../types";
|
|
80
82
|
|
|
83
|
+
const SPAWN_LAND_STRATEGIES = ["direct", "pr"] as const;
|
|
84
|
+
const SPAWN_AUTO_MERGE_POLICIES = ["on-green", "on-approval", "manual"] as const;
|
|
85
|
+
|
|
81
86
|
function rowToWorkspace(row: any): WorkspaceRecord {
|
|
82
87
|
return {
|
|
83
88
|
id: row.id,
|
|
@@ -102,6 +107,36 @@ function rowToWorkspace(row: any): WorkspaceRecord {
|
|
|
102
107
|
};
|
|
103
108
|
}
|
|
104
109
|
|
|
110
|
+
function workspaceLandMetadataFrom(value: Record<string, unknown> | undefined): {
|
|
111
|
+
landStrategy?: WorkspaceLandingPolicy;
|
|
112
|
+
autoMerge?: WorkspaceAutoMergePolicy;
|
|
113
|
+
reviewer?: string;
|
|
114
|
+
} {
|
|
115
|
+
if (!value) return {};
|
|
116
|
+
const landStrategy = typeof value.landStrategy === "string" && (SPAWN_LAND_STRATEGIES as readonly string[]).includes(value.landStrategy)
|
|
117
|
+
? value.landStrategy as WorkspaceLandingPolicy
|
|
118
|
+
: undefined;
|
|
119
|
+
const autoMerge = typeof value.autoMerge === "string" && (SPAWN_AUTO_MERGE_POLICIES as readonly string[]).includes(value.autoMerge)
|
|
120
|
+
? value.autoMerge as WorkspaceAutoMergePolicy
|
|
121
|
+
: undefined;
|
|
122
|
+
const reviewer = typeof value.reviewer === "string" && value.reviewer.trim() ? value.reviewer.trim() : undefined;
|
|
123
|
+
return {
|
|
124
|
+
...(landStrategy ? { landStrategy } : {}),
|
|
125
|
+
...(autoMerge ? { autoMerge } : {}),
|
|
126
|
+
...(reviewer ? { reviewer } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function spawnCommandLandMetadata(spawnRequestId: string | undefined): ReturnType<typeof workspaceLandMetadataFrom> {
|
|
131
|
+
if (!spawnRequestId) return {};
|
|
132
|
+
const row = getDb().query(`
|
|
133
|
+
SELECT params FROM commands
|
|
134
|
+
WHERE type = 'agent.spawn' AND correlation_id = ?
|
|
135
|
+
ORDER BY created_at DESC
|
|
136
|
+
LIMIT 1
|
|
137
|
+
`).get(spawnRequestId) as { params?: string } | undefined;
|
|
138
|
+
return workspaceLandMetadataFrom(parseJson<Record<string, unknown>>(row?.params, {}));
|
|
139
|
+
}
|
|
105
140
|
|
|
106
141
|
export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord | null {
|
|
107
142
|
const workspace = agent.workspace;
|
|
@@ -140,7 +175,15 @@ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceR
|
|
|
140
175
|
ownerAgentId: agent.agentId || undefined,
|
|
141
176
|
ownerPolicyName: agent.policyName,
|
|
142
177
|
ownerAutomationRunId: agent.automationRunId,
|
|
143
|
-
metadata: {
|
|
178
|
+
metadata: {
|
|
179
|
+
...(existing?.metadata ?? {}),
|
|
180
|
+
...spawnCommandLandMetadata(agent.spawnRequestId),
|
|
181
|
+
...workspaceLandMetadataFrom(workspace as unknown as Record<string, unknown>),
|
|
182
|
+
provider: agent.provider,
|
|
183
|
+
label: agent.label,
|
|
184
|
+
sessionName: agent.sessionName,
|
|
185
|
+
tmuxSession: agent.tmuxSession,
|
|
186
|
+
},
|
|
144
187
|
});
|
|
145
188
|
}
|
|
146
189
|
// Shared-mode occupancy: one row per agent sharing a git repo, with no
|
|
@@ -438,4 +481,3 @@ export function electWorkspaceStewardsForAgent(agentId: string): void {
|
|
|
438
481
|
`).all(agentId) as Array<{ repo_root: string }>;
|
|
439
482
|
for (const r of repos) electWorkspaceStewards(r.repo_root);
|
|
440
483
|
}
|
|
441
|
-
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TOKEN_RECORD_RETENTION_SECONDS,
|
|
12
12
|
WORKSPACE_AUTO_MERGE_INTERVAL_MS,
|
|
13
13
|
WORKSPACE_GC_INTERVAL_MS,
|
|
14
|
+
WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
|
|
14
15
|
WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
|
|
15
16
|
WORKSPACE_RETENTION_MS,
|
|
16
17
|
WORKSPACE_REVIEW_TTL_MS,
|
|
@@ -45,6 +46,7 @@ export {
|
|
|
45
46
|
TOKEN_RECORD_RETENTION_SECONDS,
|
|
46
47
|
WORKSPACE_AUTO_MERGE_INTERVAL_MS,
|
|
47
48
|
WORKSPACE_GC_INTERVAL_MS,
|
|
49
|
+
WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
|
|
48
50
|
WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
|
|
49
51
|
WORKSPACE_RETENTION_MS,
|
|
50
52
|
WORKSPACE_REVIEW_TTL_MS,
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
QUOTA_HISTORY_RETENTION_MS,
|
|
54
54
|
TOKEN_RECORD_RETENTION_SECONDS,
|
|
55
55
|
WORKSPACE_AUTO_MERGE_INTERVAL_MS,
|
|
56
|
+
WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
|
|
56
57
|
WORKSPACE_GC_INTERVAL_MS,
|
|
57
58
|
WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
|
|
58
59
|
CONFLICT_SCAN_INTERVAL_MS,
|
|
@@ -61,7 +62,7 @@ import {
|
|
|
61
62
|
import { emitCommand } from "./events";
|
|
62
63
|
import { reapOrphanedSessions } from "./orphaned-sessions";
|
|
63
64
|
import { reapZeroMemberTeams, runTeamIdleWatchdog } from "./teams";
|
|
64
|
-
import { scanWorkspaceConflicts, workspaceGC } from "./workspaces";
|
|
65
|
+
import { refreshIdleWorkspaces, scanWorkspaceConflicts, workspaceGC } from "./workspaces";
|
|
65
66
|
import type { MaintenanceJobDefinition } from "./types";
|
|
66
67
|
|
|
67
68
|
let dbMaintenanceRuns = 0;
|
|
@@ -227,6 +228,14 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
227
228
|
runOnStart: true,
|
|
228
229
|
handler() {
|
|
229
230
|
const expired = expireStaleBusAgents();
|
|
231
|
+
for (const id of expired.agentIds) {
|
|
232
|
+
getLifecycleManager().onAgentDisappeared(id);
|
|
233
|
+
// #636/#659 — only after the stale reconnect grace expires do we turn a bare
|
|
234
|
+
// heartbeat gap into a terminal parent notification. A live runner that
|
|
235
|
+
// reconnects before this point heals from `stale` without a false exit.
|
|
236
|
+
const diagnosis = diagnoseAgentDeath({ agent: getAgent(id), reason: "heartbeat lost" });
|
|
237
|
+
notifyAgentOffline(id, "heartbeat lost", diagnosis);
|
|
238
|
+
}
|
|
230
239
|
for (const task of expired.orphanedTasks) emitTaskChanged(task, "task.updated");
|
|
231
240
|
return {
|
|
232
241
|
expiredBusAgentIds: expired.agentIds,
|
|
@@ -244,16 +253,11 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
244
253
|
const reapedAgentIds = reapStaleAgents(STALE_TTL_MS);
|
|
245
254
|
for (const id of reapedAgentIds) {
|
|
246
255
|
emitAgentStatus(id);
|
|
247
|
-
getLifecycleManager().onAgentDisappeared(id);
|
|
248
|
-
// #636 — a heartbeat-lost death still gets a best-effort post-mortem from the agent's last
|
|
249
|
-
// known meta (lastExit/lastError/auth) + heartbeat gap, riding the terminal event.
|
|
250
|
-
const diagnosis = diagnoseAgentDeath({ agent: getAgent(id), reason: "heartbeat lost" });
|
|
251
|
-
notifyAgentOffline(id, "heartbeat lost", diagnosis);
|
|
252
256
|
createActivityEvent({
|
|
253
257
|
clientId: "server-agent-" + id + "-heartbeat-lost-" + Date.now(),
|
|
254
258
|
kind: "state",
|
|
255
|
-
title: "Heartbeat
|
|
256
|
-
body: "Agent
|
|
259
|
+
title: "Heartbeat stale",
|
|
260
|
+
body: "Agent entered reconnect grace",
|
|
257
261
|
meta: id,
|
|
258
262
|
icon: "ti-heart-off",
|
|
259
263
|
view: "chat",
|
|
@@ -442,6 +446,15 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
442
446
|
timeoutMs: 60 * 1000,
|
|
443
447
|
handler: autoMergeCleanFastForwards,
|
|
444
448
|
},
|
|
449
|
+
{
|
|
450
|
+
id: "workspace-idle-refresh",
|
|
451
|
+
title: "Workspace idle refresh",
|
|
452
|
+
description: "Proactively advance idle branch-agent workspaces to origin/main (#478): for each active, clean (dirty=0, ahead=0), owner-idle workspace, dispatch a workspace.idle-refresh command so the agent wakes already current and needs no ready/status dance.",
|
|
453
|
+
intervalMs: WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
|
|
454
|
+
runOnStart: false,
|
|
455
|
+
timeoutMs: 60 * 1000,
|
|
456
|
+
handler: refreshIdleWorkspaces,
|
|
457
|
+
},
|
|
445
458
|
{
|
|
446
459
|
id: "workspace-gc",
|
|
447
460
|
title: "Workspace GC",
|