agent-relay-server 0.119.7 → 0.119.10
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 +3 -3
- package/public/assets/{MessageBubble-Azda2GIf.js → MessageBubble-U5sX3SNZ.js} +2 -2
- package/public/assets/{MessageBubble-Azda2GIf.js.map → MessageBubble-U5sX3SNZ.js.map} +1 -1
- package/public/assets/PlanGraphPanel-Bi2gvXeE.js +2 -0
- package/public/assets/PlanGraphPanel-Bi2gvXeE.js.map +1 -0
- package/public/assets/{activity-B0hz4XPy.js → activity-Cwkz51pG.js} +2 -2
- package/public/assets/{activity-B0hz4XPy.js.map → activity-Cwkz51pG.js.map} +1 -1
- package/public/assets/{agent-profiles-BF0oSOTq.js → agent-profiles-BgddVqRp.js} +2 -2
- package/public/assets/{agent-profiles-BF0oSOTq.js.map → agent-profiles-BgddVqRp.js.map} +1 -1
- package/public/assets/{agents-CW4--j3Q.js → agents-CiGYBw0h.js} +2 -2
- package/public/assets/{agents-CW4--j3Q.js.map → agents-CiGYBw0h.js.map} +1 -1
- package/public/assets/{automation-S6_bQ2t-.js → automation-BBJB4RaE.js} +2 -2
- package/public/assets/{automation-S6_bQ2t-.js.map → automation-BBJB4RaE.js.map} +1 -1
- package/public/assets/{chat-hoFhhvgy.js → chat-DS578gZy.js} +2 -2
- package/public/assets/{chat-hoFhhvgy.js.map → chat-DS578gZy.js.map} +1 -1
- package/public/assets/{formatted-body-BHm145gP.js → formatted-body-DYUVqWym.js} +3 -3
- package/public/assets/{formatted-body-BHm145gP.js.map → formatted-body-DYUVqWym.js.map} +1 -1
- package/public/assets/{formatted-body-impl-CvE31j9F.js → formatted-body-impl-DdcyxVO0.js} +2 -2
- package/public/assets/{formatted-body-impl-CvE31j9F.js.map → formatted-body-impl-DdcyxVO0.js.map} +1 -1
- package/public/assets/{index-TamtghSr.js → index-CqcAqQKW.js} +6 -6
- package/public/assets/{index-TamtghSr.js.map → index-CqcAqQKW.js.map} +1 -1
- package/public/assets/{insights-Ci9ftVfl.js → insights-BunHbsTA.js} +2 -2
- package/public/assets/{insights-Ci9ftVfl.js.map → insights-BunHbsTA.js.map} +1 -1
- package/public/assets/{maintenance-bVhNbr33.js → maintenance-cICBVcpJ.js} +2 -2
- package/public/assets/{maintenance-bVhNbr33.js.map → maintenance-cICBVcpJ.js.map} +1 -1
- package/public/assets/{managed-agents-D5HO4dhI.js → managed-agents-BK9_Gi-M.js} +2 -2
- package/public/assets/{managed-agents-D5HO4dhI.js.map → managed-agents-BK9_Gi-M.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DO-p83dz.js → markdown-preview-impl-EZfcTHG9.js} +2 -2
- package/public/assets/{markdown-preview-impl-DO-p83dz.js.map → markdown-preview-impl-EZfcTHG9.js.map} +1 -1
- package/public/assets/{memory-D72o8umQ.js → memory-Vi-8HvK3.js} +2 -2
- package/public/assets/{memory-D72o8umQ.js.map → memory-Vi-8HvK3.js.map} +1 -1
- package/public/assets/{messages-5L_Bc_9w.js → messages-DScIiBMI.js} +2 -2
- package/public/assets/{messages-5L_Bc_9w.js.map → messages-DScIiBMI.js.map} +1 -1
- package/public/assets/{orchestrators-CnfgkQpl.js → orchestrators-DLogldXx.js} +2 -2
- package/public/assets/{orchestrators-CnfgkQpl.js.map → orchestrators-DLogldXx.js.map} +1 -1
- package/public/assets/{overview-Bu39NIyR.js → overview-B_RQAQR8.js} +2 -2
- package/public/assets/{overview-Bu39NIyR.js.map → overview-B_RQAQR8.js.map} +1 -1
- package/public/assets/{pairs-WuamP5W5.js → pairs-CSiPQ7E2.js} +2 -2
- package/public/assets/{pairs-WuamP5W5.js.map → pairs-CSiPQ7E2.js.map} +1 -1
- package/public/assets/{prompt-settings-BCrQreU6.js → prompt-settings-DsDi_Xpd.js} +2 -2
- package/public/assets/{prompt-settings-BCrQreU6.js.map → prompt-settings-DsDi_Xpd.js.map} +1 -1
- package/public/assets/{registry-kHKn0xwg.js → registry-DHFUWIdb.js} +2 -2
- package/public/assets/{registry-kHKn0xwg.js.map → registry-DHFUWIdb.js.map} +1 -1
- package/public/assets/{security-Dk5t3r_K.js → security-D2nHkJH6.js} +2 -2
- package/public/assets/{security-Dk5t3r_K.js.map → security-D2nHkJH6.js.map} +1 -1
- package/public/assets/{select-BdsZQfU2.js → select-BjNcommA.js} +2 -2
- package/public/assets/{select-BdsZQfU2.js.map → select-BjNcommA.js.map} +1 -1
- package/public/assets/{settings-EpjzQHnZ.js → settings-DiUEq54n.js} +2 -2
- package/public/assets/{settings-EpjzQHnZ.js.map → settings-DiUEq54n.js.map} +1 -1
- package/public/assets/{tasks-CedMQHbg.js → tasks-Dx8GVQiR.js} +2 -2
- package/public/assets/{tasks-CedMQHbg.js.map → tasks-Dx8GVQiR.js.map} +1 -1
- package/public/assets/{teams-BlHBo5Qt.js → teams-DQtf7bha.js} +2 -2
- package/public/assets/{teams-BlHBo5Qt.js.map → teams-DQtf7bha.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-gGqYpsjq.js → terminal-viewer-impl-D5rhXV4u.js} +2 -2
- package/public/assets/{terminal-viewer-impl-gGqYpsjq.js.map → terminal-viewer-impl-D5rhXV4u.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-D7opMqtU.js → work-queue-BfWNDp7U.js} +2 -2
- package/public/assets/{work-queue-D7opMqtU.js.map → work-queue-BfWNDp7U.js.map} +1 -1
- package/public/index.html +2 -2
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +13 -0
- package/src/db/orchestrators.ts +1 -1
- package/src/db/provider-quotas.ts +2 -34
- package/src/db/schema.ts +3 -5
- package/src/db/work-evidence.ts +38 -0
- package/src/db/workspaces.ts +7 -3
- package/src/lifecycle-manager.ts +2 -4
- package/src/maintenance/jobs.ts +10 -1
- package/src/maintenance/workspaces.ts +71 -13
- package/src/routes/commands.ts +20 -4
- package/src/routes/workspaces.ts +4 -7
- package/src/services/transient-agent-reaper.ts +54 -9
- package/src/workspace-actions.ts +3 -4
- package/src/workspace-auto-merge.ts +95 -13
- package/src/workspace-host.ts +55 -0
- package/src/workspace-merge.ts +3 -4
- package/src/workspace-orphans.ts +78 -55
- package/src/workspace-probe.ts +2 -3
- package/public/assets/PlanGraphPanel-B9TN_KQr.js +0 -2
- package/public/assets/PlanGraphPanel-B9TN_KQr.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{An as e,a as t,ht as n,nr as r,vn as i}from"./lucide-react-Dc3ZwCBi.js";import{f as a,t as o}from"./store-OLe8gFh0.js";import{K as s,ct as c,d as l}from"./display-exQDWum3.js";import{t as u}from"./badge-BhUZ1cAb.js";import{t as d}from"./button-C-V4oij1.js";import{Ot as f,St as p,U as m}from"./index-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
1
|
+
import{An as e,a as t,ht as n,nr as r,vn as i}from"./lucide-react-Dc3ZwCBi.js";import{f as a,t as o}from"./store-OLe8gFh0.js";import{K as s,ct as c,d as l}from"./display-exQDWum3.js";import{t as u}from"./badge-BhUZ1cAb.js";import{t as d}from"./button-C-V4oij1.js";import{Ot as f,St as p,U as m}from"./index-CqcAqQKW.js";import{r as h,t as g}from"./card-Br1JLjTH.js";var _=r(),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=p(),S=f(),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`?`T${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:n,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[n.severity]||c.info,m=v[n.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:n.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:n.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:n.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:n.status}),n.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`})]}),n.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:n.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[n.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(n.owner)]}),n.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:n.source}),(0,_.jsxs)(`span`,{title:String(n.updatedAt),children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),s(r,n.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[n.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)(i,{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)(t,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
|
|
2
|
+
//# sourceMappingURL=work-queue-BfWNDp7U.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-queue-D7opMqtU.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' ? `T${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-BfWNDp7U.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' ? `T${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-CqcAqQKW.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-C_JMBXql.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/lucide-react-Dc3ZwCBi.js">
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<link rel="modulepreload" crossorigin href="./assets/agent-type-icon-BEz393ra.js">
|
|
37
37
|
<link rel="modulepreload" crossorigin href="./assets/button-C-V4oij1.js">
|
|
38
38
|
<link rel="modulepreload" crossorigin href="./assets/copy-button-BStzXgtd.js">
|
|
39
|
-
<link rel="modulepreload" crossorigin href="./assets/formatted-body-
|
|
39
|
+
<link rel="modulepreload" crossorigin href="./assets/formatted-body-DYUVqWym.js">
|
|
40
40
|
<link rel="modulepreload" crossorigin href="./assets/task-chip-kxk4eNm1.js">
|
|
41
41
|
<link rel="modulepreload" crossorigin href="./assets/badge-BhUZ1cAb.js">
|
|
42
42
|
<link rel="modulepreload" crossorigin href="./assets/input-CtLT7WXi.js">
|
package/src/db/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./integrations.ts";
|
|
|
16
16
|
export * from "./pairs.ts";
|
|
17
17
|
export * from "./artifacts.ts";
|
|
18
18
|
export * from "./messages.ts";
|
|
19
|
+
export * from "./work-evidence.ts";
|
|
19
20
|
export * from "./delivery.ts";
|
|
20
21
|
export * from "./message-reads.ts";
|
|
21
22
|
export * from "./inbox.ts";
|
package/src/db/migrations.ts
CHANGED
|
@@ -218,10 +218,19 @@ export function applyMigrations(): void {
|
|
|
218
218
|
`);
|
|
219
219
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_workspace_merge_pauses_expires ON workspace_merge_pauses(expires_at)");
|
|
220
220
|
|
|
221
|
+
const workspaceCols = getDb().query("PRAGMA table_info(workspaces)").all() as any[];
|
|
222
|
+
const workspaceColNames = workspaceCols.map((c: any) => c.name);
|
|
223
|
+
if (!workspaceColNames.includes("orchestrator_id")) {
|
|
224
|
+
getDb().run("ALTER TABLE workspaces ADD COLUMN orchestrator_id TEXT");
|
|
225
|
+
}
|
|
226
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_workspaces_orchestrator ON workspaces(orchestrator_id)");
|
|
227
|
+
|
|
221
228
|
const cols = getDb().query("PRAGMA table_info(messages)").all() as any[];
|
|
222
229
|
const colNames = cols.map((c: any) => c.name);
|
|
223
230
|
if (!colNames.includes("thread_id")) {
|
|
224
231
|
getDb().run("ALTER TABLE messages ADD COLUMN thread_id INTEGER");
|
|
232
|
+
}
|
|
233
|
+
if (!colNames.includes("reply_to")) {
|
|
225
234
|
getDb().run("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
|
|
226
235
|
}
|
|
227
236
|
if (!colNames.includes("reply_expected")) {
|
|
@@ -274,6 +283,10 @@ export function applyMigrations(): void {
|
|
|
274
283
|
"UPDATE messages SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
|
|
275
284
|
).run(Date.now(), CLAIM_LEASE_MS);
|
|
276
285
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_thread ON messages(thread_id)");
|
|
286
|
+
// (reply_to, from_agent) powers the reply-obligation NOT EXISTS subquery in
|
|
287
|
+
// listPendingReplyObligations. It must stay migration-owned because reply_to is
|
|
288
|
+
// an ALTER-added column on legacy DBs (#963).
|
|
289
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent)");
|
|
277
290
|
getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key) WHERE idempotency_key IS NOT NULL");
|
|
278
291
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_delivery_status ON messages(delivery_status)");
|
|
279
292
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_claimed_expires ON messages(claimed_by, claim_expires_at, id) WHERE claimed_by IS NOT NULL");
|
package/src/db/orchestrators.ts
CHANGED
|
@@ -309,7 +309,7 @@ export function orchestratorHeartbeat(id: string, runtime: OrchestratorRuntimeIn
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
export function updateManagedAgents(id: string, agents: ManagedAgent[]): Orchestrator | null {
|
|
312
|
-
for (const agent of agents) upsertWorkspaceFromManagedAgent(agent);
|
|
312
|
+
for (const agent of agents) upsertWorkspaceFromManagedAgent(agent, id);
|
|
313
313
|
electWorkspaceStewards();
|
|
314
314
|
const enriched = agents.map((agent) => {
|
|
315
315
|
const workspaceId = agent.workspace?.id;
|
|
@@ -8,9 +8,7 @@ import { parseStringArray } from "./shared.ts";
|
|
|
8
8
|
import type { AgentCard, CodexResetCredits, ProviderQuotaError, ProviderQuotaMatrix, ProviderQuotaRecord, ProviderQuotaSnapshot, ProviderQuotaUpdateInput, QuotaState } from "../types";
|
|
9
9
|
|
|
10
10
|
const PROVIDER_QUOTA_STALE_AFTER_MS = 10 * 60_000;
|
|
11
|
-
const QUOTA_UTILIZATION_EPSILON = 1e-9;
|
|
12
11
|
const QUOTA_RESET_BOUNDARY_ADVANCE_MS = 5_000;
|
|
13
|
-
const QUOTA_RESET_UTILIZATION_MAX = 0.01;
|
|
14
12
|
const FIVE_HOUR_MS = 5 * 60 * 60_000;
|
|
15
13
|
|
|
16
14
|
registerAccountKeyAliases("codex", (accountKey) => {
|
|
@@ -250,30 +248,6 @@ function quotaWindowKey(window: QuotaState["windows"][number]): string {
|
|
|
250
248
|
return `${window.name}:${window.unit}`;
|
|
251
249
|
}
|
|
252
250
|
|
|
253
|
-
function quotaResetBoundaryAdvanced(existing: QuotaState["windows"][number], incoming: QuotaState["windows"][number]): boolean {
|
|
254
|
-
return quotaResetBoundaryMoved(existing, incoming)
|
|
255
|
-
&& incoming.resetsAt! - existing.resetsAt! > QUOTA_RESET_BOUNDARY_ADVANCE_MS;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function quotaResetBoundaryMoved(existing: QuotaState["windows"][number], incoming: QuotaState["windows"][number]): boolean {
|
|
259
|
-
return typeof existing.resetsAt === "number"
|
|
260
|
-
&& Number.isFinite(existing.resetsAt)
|
|
261
|
-
&& typeof incoming.resetsAt === "number"
|
|
262
|
-
&& Number.isFinite(incoming.resetsAt)
|
|
263
|
-
&& incoming.resetsAt > existing.resetsAt;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function quotaWindowLooksReset(existing: QuotaState["windows"][number], incoming: QuotaState["windows"][number]): boolean {
|
|
267
|
-
return incoming.utilization <= QUOTA_RESET_UTILIZATION_MAX
|
|
268
|
-
&& quotaResetBoundaryMoved(existing, incoming);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function quotaWindowUtilizationRegressed(existing: QuotaState["windows"][number], incoming: QuotaState["windows"][number]): boolean {
|
|
272
|
-
return incoming.utilization + QUOTA_UTILIZATION_EPSILON < existing.utilization
|
|
273
|
-
&& !quotaResetBoundaryAdvanced(existing, incoming)
|
|
274
|
-
&& !quotaWindowLooksReset(existing, incoming);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
251
|
function publicQuotaState(quota: StoredQuotaState, now?: number): QuotaState {
|
|
278
252
|
let emulated = false;
|
|
279
253
|
const windows = quota.windows.map((window) => {
|
|
@@ -404,8 +378,7 @@ function mergeQuotaWindows(existing: StoredQuotaState | undefined, incoming: Quo
|
|
|
404
378
|
// Per-window source precedence: #712/#716 are handled by the symmetric three-way split below.
|
|
405
379
|
// Higher existing → keep (no regression check needed; lower-priority input simply loses).
|
|
406
380
|
// Higher incoming → accept (no regression check needed; higher-priority source is authoritative).
|
|
407
|
-
// Same priority →
|
|
408
|
-
// same-quality sources, e.g. a wedged probe self-report; #725 §4.3).
|
|
381
|
+
// Same priority → newest per-window sample wins; older/equal samples are stale.
|
|
409
382
|
const existingPriority = quotaSourcePriority(existingSource);
|
|
410
383
|
const incomingPriority = quotaSourcePriority(incoming.source);
|
|
411
384
|
if (existingPriority > incomingPriority) {
|
|
@@ -418,12 +391,7 @@ function mergeQuotaWindows(existing: StoredQuotaState | undefined, incoming: Quo
|
|
|
418
391
|
mergedTimes.set(key, incoming.updatedAt); mergedSources.set(key, incoming.source);
|
|
419
392
|
return incomingWindow;
|
|
420
393
|
}
|
|
421
|
-
|
|
422
|
-
// self-reports, but let authoritative API ingest correct a stale/maxed WHAM sample (#750).
|
|
423
|
-
if (incoming.updatedAt < existingUpdatedAt) {
|
|
424
|
-
mergedTimes.set(key, existingUpdatedAt); mergedSources.set(key, existingSource); return window;
|
|
425
|
-
}
|
|
426
|
-
if (incoming.source !== "ingest" && quotaWindowUtilizationRegressed(window, incomingWindow)) {
|
|
394
|
+
if (incoming.updatedAt <= existingUpdatedAt) {
|
|
427
395
|
mergedTimes.set(key, existingUpdatedAt); mergedSources.set(key, existingSource); return window;
|
|
428
396
|
}
|
|
429
397
|
acceptedIncomingWindow = true;
|
package/src/db/schema.ts
CHANGED
|
@@ -163,14 +163,10 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
163
163
|
-- queries (delivery, polling, etc.) are served identically.
|
|
164
164
|
-- idx_msg_resolved_to_id is created in applyMigrations() AFTER the
|
|
165
165
|
-- resolved_to_agent ALTER — never inline here (same hazard as #483/#559).
|
|
166
|
+
-- idx_msg_reply_to is also migration-owned because reply_to was added by ALTER.
|
|
166
167
|
CREATE INDEX IF NOT EXISTS idx_msg_to_id ON messages(to_target, id);
|
|
167
168
|
CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(created_at);
|
|
168
169
|
CREATE INDEX IF NOT EXISTS idx_msg_channel ON messages(channel);
|
|
169
|
-
-- (reply_to, from_agent) powers the reply-obligation NOT EXISTS subquery in
|
|
170
|
-
-- listPendingReplyObligations. Without it that subquery full-scans messages
|
|
171
|
-
-- per candidate row (O(n^2)): ~8.6s at 4k rows, which blew the 5s Stop-hook
|
|
172
|
-
-- timeout and wedged turns in "busy" (#199).
|
|
173
|
-
CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent);
|
|
174
170
|
CREATE TABLE IF NOT EXISTS scheduled_timers (
|
|
175
171
|
id TEXT PRIMARY KEY,
|
|
176
172
|
target TEXT NOT NULL,
|
|
@@ -585,6 +581,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
585
581
|
mode TEXT NOT NULL,
|
|
586
582
|
requested_mode TEXT,
|
|
587
583
|
status TEXT NOT NULL DEFAULT 'active',
|
|
584
|
+
orchestrator_id TEXT,
|
|
588
585
|
owner_agent_id TEXT,
|
|
589
586
|
owner_policy_name TEXT,
|
|
590
587
|
owner_automation_run_id TEXT,
|
|
@@ -596,6 +593,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
596
593
|
cleaned_at INTEGER
|
|
597
594
|
);
|
|
598
595
|
CREATE INDEX IF NOT EXISTS idx_workspaces_repo_status ON workspaces(repo_root, status);
|
|
596
|
+
-- idx_workspaces_orchestrator created in applyMigrations() AFTER orchestrator_id ALTER (#949)
|
|
599
597
|
CREATE INDEX IF NOT EXISTS idx_workspaces_owner_agent ON workspaces(owner_agent_id);
|
|
600
598
|
CREATE INDEX IF NOT EXISTS idx_workspaces_policy ON workspaces(owner_policy_name);
|
|
601
599
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getDb } from "./connection.ts";
|
|
2
|
+
import { MSG_SELECT, rowToMessage } from "./mappers.ts";
|
|
3
|
+
import type { Message } from "../types";
|
|
4
|
+
|
|
5
|
+
export function hasArtifactCreatedBy(createdBy: string, since: number = 0): boolean {
|
|
6
|
+
const row = getDb().query(`
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM artifacts a
|
|
9
|
+
JOIN artifact_blobs b ON b.digest = a.blob_digest
|
|
10
|
+
WHERE a.created_by = ?
|
|
11
|
+
AND a.created_at >= ?
|
|
12
|
+
AND a.size > 0
|
|
13
|
+
AND b.size > 0
|
|
14
|
+
AND EXISTS (
|
|
15
|
+
SELECT 1
|
|
16
|
+
FROM artifact_links l
|
|
17
|
+
WHERE l.artifact_id = a.id
|
|
18
|
+
AND l.entity_type IN ('message', 'task')
|
|
19
|
+
)
|
|
20
|
+
LIMIT 1
|
|
21
|
+
`).get(createdBy, since);
|
|
22
|
+
return Boolean(row);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function listFinalResponseCaptureMessages(from: string, since: number = 0, limit: number = 20): Message[] {
|
|
26
|
+
const safeLimit = Math.min(Math.max(Math.floor(limit), 1), 100);
|
|
27
|
+
const rows = getDb().query(`
|
|
28
|
+
${MSG_SELECT}
|
|
29
|
+
WHERE m.from_agent = ?
|
|
30
|
+
AND m.kind = 'chat'
|
|
31
|
+
AND m.created_at >= ?
|
|
32
|
+
AND json_extract(m.payload, '$.responseCapture.type') = 'response'
|
|
33
|
+
AND json_extract(m.payload, '$.responseCapture.final') = 1
|
|
34
|
+
ORDER BY m.id DESC
|
|
35
|
+
LIMIT ?
|
|
36
|
+
`).all(from, since, safeLimit) as any[];
|
|
37
|
+
return rows.map(rowToMessage);
|
|
38
|
+
}
|
package/src/db/workspaces.ts
CHANGED
|
@@ -97,6 +97,7 @@ function rowToWorkspace(row: any): WorkspaceRecord {
|
|
|
97
97
|
mode: row.mode,
|
|
98
98
|
requestedMode: row.requested_mode ?? undefined,
|
|
99
99
|
status: row.status,
|
|
100
|
+
orchestratorId: row.orchestrator_id ?? undefined,
|
|
100
101
|
ownerAgentId: row.owner_agent_id ?? undefined,
|
|
101
102
|
ownerPolicyName: row.owner_policy_name ?? undefined,
|
|
102
103
|
ownerAutomationRunId: row.owner_automation_run_id ?? undefined,
|
|
@@ -140,7 +141,7 @@ function spawnCommandLandMetadata(spawnRequestId: string | undefined): ReturnTyp
|
|
|
140
141
|
return workspaceLandMetadataFrom(parseJson<Record<string, unknown>>(row?.params, {}));
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord | null {
|
|
144
|
+
export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent, orchestratorId?: string): WorkspaceRecord | null {
|
|
144
145
|
const workspace = agent.workspace;
|
|
145
146
|
if (!workspace) return null;
|
|
146
147
|
if (workspace.mode === "isolated" && workspace.id && workspace.repoRoot && workspace.worktreePath) {
|
|
@@ -174,6 +175,7 @@ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceR
|
|
|
174
175
|
mode: workspace.mode,
|
|
175
176
|
requestedMode: workspace.requestedMode,
|
|
176
177
|
status: preserveStatus ? existing!.status : (workspace.status ?? "active"),
|
|
178
|
+
orchestratorId: existing?.orchestratorId ?? orchestratorId,
|
|
177
179
|
ownerAgentId: agent.agentId || undefined,
|
|
178
180
|
ownerPolicyName: agent.policyName,
|
|
179
181
|
ownerAutomationRunId: agent.automationRunId,
|
|
@@ -215,8 +217,8 @@ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceR
|
|
|
215
217
|
export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "updatedAt"> & Partial<Pick<WorkspaceRecord, "createdAt" | "updatedAt">>): WorkspaceRecord {
|
|
216
218
|
const now = Date.now();
|
|
217
219
|
getDb().query(`
|
|
218
|
-
INSERT INTO workspaces (id, repo_root, source_cwd, worktree_path, branch, base_ref, base_sha, mode, requested_mode, status, owner_agent_id, owner_policy_name, owner_automation_run_id, steward_agent_id, metadata, created_at, updated_at, ready_at, cleaned_at)
|
|
219
|
-
VALUES ($id, $repoRoot, $sourceCwd, $worktreePath, $branch, $baseRef, $baseSha, $mode, $requestedMode, $status, $ownerAgentId, $ownerPolicyName, $ownerAutomationRunId, $stewardAgentId, $metadata, $createdAt, $updatedAt, $readyAt, $cleanedAt)
|
|
220
|
+
INSERT INTO workspaces (id, repo_root, source_cwd, worktree_path, branch, base_ref, base_sha, mode, requested_mode, status, orchestrator_id, owner_agent_id, owner_policy_name, owner_automation_run_id, steward_agent_id, metadata, created_at, updated_at, ready_at, cleaned_at)
|
|
221
|
+
VALUES ($id, $repoRoot, $sourceCwd, $worktreePath, $branch, $baseRef, $baseSha, $mode, $requestedMode, $status, $orchestratorId, $ownerAgentId, $ownerPolicyName, $ownerAutomationRunId, $stewardAgentId, $metadata, $createdAt, $updatedAt, $readyAt, $cleanedAt)
|
|
220
222
|
ON CONFLICT(id) DO UPDATE SET
|
|
221
223
|
repo_root = excluded.repo_root,
|
|
222
224
|
source_cwd = excluded.source_cwd,
|
|
@@ -227,6 +229,7 @@ export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "upda
|
|
|
227
229
|
mode = excluded.mode,
|
|
228
230
|
requested_mode = excluded.requested_mode,
|
|
229
231
|
status = excluded.status,
|
|
232
|
+
orchestrator_id = coalesce(excluded.orchestrator_id, workspaces.orchestrator_id),
|
|
230
233
|
owner_agent_id = coalesce(excluded.owner_agent_id, workspaces.owner_agent_id),
|
|
231
234
|
owner_policy_name = coalesce(excluded.owner_policy_name, workspaces.owner_policy_name),
|
|
232
235
|
owner_automation_run_id = coalesce(excluded.owner_automation_run_id, workspaces.owner_automation_run_id),
|
|
@@ -246,6 +249,7 @@ export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "upda
|
|
|
246
249
|
$mode: input.mode,
|
|
247
250
|
$requestedMode: input.requestedMode ?? null,
|
|
248
251
|
$status: input.status,
|
|
252
|
+
$orchestratorId: input.orchestratorId ?? null,
|
|
249
253
|
$ownerAgentId: input.ownerAgentId ?? null,
|
|
250
254
|
$ownerPolicyName: input.ownerPolicyName ?? null,
|
|
251
255
|
$ownerAutomationRunId: input.ownerAutomationRunId ?? null,
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCommand } from "./commands-db";
|
|
2
|
-
import {
|
|
2
|
+
import { workspaceOwnerOrchestrator } from "./workspace-host";
|
|
3
3
|
import { agentProviderState } from "./provider-state";
|
|
4
4
|
import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces } from "./db";
|
|
5
5
|
import {
|
|
@@ -584,9 +584,7 @@ export class LifecycleManager {
|
|
|
584
584
|
if (!candidates.length) return;
|
|
585
585
|
const orchestrators = listOrchestrators();
|
|
586
586
|
for (const ws of candidates) {
|
|
587
|
-
const orch = orchestrators
|
|
588
|
-
(candidate) => candidate.status === "online" && isPathWithinBase(ws.sourceCwd, candidate.baseDir),
|
|
589
|
-
);
|
|
587
|
+
const orch = workspaceOwnerOrchestrator(ws, orchestrators);
|
|
590
588
|
if (!orch) continue;
|
|
591
589
|
const command = createCommand({
|
|
592
590
|
type: "workspace.reconcile",
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { isAgentUnavailable } from "../db/agent-predicates";
|
|
|
27
27
|
import { flushQueuedDirectMessages } from "../services/managed-running";
|
|
28
28
|
import { notifyAgentOffline } from "../agent-lifecycle-events";
|
|
29
29
|
import { diagnoseAgentDeath } from "../agent-death-diagnosis";
|
|
30
|
-
import { autoMergeCleanFastForwards } from "../workspace-auto-merge";
|
|
30
|
+
import { autoMergeCleanFastForwards, runAutoMergeWatchdog } from "../workspace-auto-merge";
|
|
31
31
|
import { reapOrphanedWorktrees } from "../workspace-orphans";
|
|
32
32
|
import { getLifecycleManager } from "../lifecycle-manager";
|
|
33
33
|
import { applyCommandToRecipe } from "../recipe-runner";
|
|
@@ -485,6 +485,15 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
485
485
|
timeoutMs: 60 * 1000,
|
|
486
486
|
handler: autoMergeCleanFastForwards,
|
|
487
487
|
},
|
|
488
|
+
{
|
|
489
|
+
id: "workspace-auto-merge-watchdog",
|
|
490
|
+
title: "Workspace auto-merge watchdog",
|
|
491
|
+
description: "Restarts stale per-repo auto-merge loops when ready/review_requested work exists but no auto-merge cycle has completed recently.",
|
|
492
|
+
intervalMs: 60 * 1000,
|
|
493
|
+
runOnStart: false,
|
|
494
|
+
timeoutMs: 10 * 1000,
|
|
495
|
+
handler: runAutoMergeWatchdog,
|
|
496
|
+
},
|
|
488
497
|
{
|
|
489
498
|
id: "workspace-idle-refresh",
|
|
490
499
|
title: "Workspace idle refresh",
|
|
@@ -13,13 +13,14 @@ import {
|
|
|
13
13
|
updateWorkspaceStatus,
|
|
14
14
|
setWorkspaceBranch,
|
|
15
15
|
} from "../db";
|
|
16
|
-
import type { WorkspaceMergePreview, WorkspaceStatus } from "../types";
|
|
16
|
+
import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "../types";
|
|
17
17
|
import { notifyStewardHandoff, reconcileLandedWorkspace } from "../branch-landed";
|
|
18
18
|
import { resolveStewardIdentity } from "../steward-identity";
|
|
19
19
|
import { getStewardConfig } from "../config-store";
|
|
20
20
|
import { preparePrCompletionScan, reconcileLandedPr } from "../workspace-pr-completion";
|
|
21
21
|
import { applyBranchFreshness, fetchHostMergePreview, workspacePathWithinBase } from "../workspace-probe";
|
|
22
22
|
import { wakeRepoSteward } from "../workspace-auto-merge";
|
|
23
|
+
import { workspaceOwnerOrchestrator } from "../workspace-host";
|
|
23
24
|
import {
|
|
24
25
|
CONFLICT_SCAN_STATUSES,
|
|
25
26
|
DAY_MS,
|
|
@@ -44,7 +45,7 @@ export async function scanWorkspaceConflicts(): Promise<Record<string, unknown>>
|
|
|
44
45
|
const reviewerArmed: string[] = [], reviewerChangesRequested: string[] = [], relayPrMerged: string[] = [], relayPrWaiting: string[] = [];
|
|
45
46
|
|
|
46
47
|
for (const ws of candidates) {
|
|
47
|
-
const orch =
|
|
48
|
+
const orch = workspaceOwnerOrchestrator(ws, orchestrators, { requireApiUrl: true });
|
|
48
49
|
if (!orch?.apiUrl) continue;
|
|
49
50
|
const preview = await fetchHostMergePreview(orch.apiUrl, ws, { checkPr: LANDED_RECONCILE_STATUSES.has(ws.status) });
|
|
50
51
|
if (!preview || (preview as { available?: false }).available === false) continue;
|
|
@@ -129,6 +130,31 @@ export async function scanWorkspaceConflicts(): Promise<Record<string, unknown>>
|
|
|
129
130
|
return { scanned: candidates.length, flagged, cleared, merged, prRecycled, notifiedStewards, reviewerArmed, reviewerChangesRequested, relayPrMerged, relayPrWaiting };
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
type AutoAbandonDecision =
|
|
134
|
+
| { kind: "abandon" }
|
|
135
|
+
| { kind: "defer"; reason: string }
|
|
136
|
+
| { kind: "landed"; preview: WorkspaceMergePreview };
|
|
137
|
+
|
|
138
|
+
function previewProvesLanded(preview: WorkspaceMergePreview): boolean {
|
|
139
|
+
const cleanWorktree = (preview.dirtyCount ?? 0) === 0;
|
|
140
|
+
return preview.landed === true
|
|
141
|
+
|| preview.prMerged === true
|
|
142
|
+
|| preview.noop === true
|
|
143
|
+
|| (cleanWorktree && (preview.unmergedAhead === 0 || preview.ahead === 0));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function reviewAutoAbandonDecision(ws: WorkspaceRecord, orchestrators: ReturnType<typeof listOrchestrators>): Promise<AutoAbandonDecision> {
|
|
147
|
+
const owner = workspaceOwnerOrchestrator(ws, orchestrators, { requireApiUrl: true });
|
|
148
|
+
if (!owner?.apiUrl) return ws.orchestratorId ? { kind: "defer", reason: "owner-unavailable" } : { kind: "abandon" };
|
|
149
|
+
const preview = await fetchHostMergePreview(owner.apiUrl, ws, { checkPr: true });
|
|
150
|
+
if (!preview || (preview as { available?: false }).available === false) return { kind: "defer", reason: "owner-unavailable" };
|
|
151
|
+
const p = preview as WorkspaceMergePreview;
|
|
152
|
+
if (p.error) return { kind: "defer", reason: "preview-error" };
|
|
153
|
+
if (previewProvesLanded(p)) return { kind: "landed", preview: p };
|
|
154
|
+
if ((p.dirtyCount ?? 0) > 0) return { kind: "defer", reason: "dirty-worktree" };
|
|
155
|
+
return { kind: "abandon" };
|
|
156
|
+
}
|
|
157
|
+
|
|
132
158
|
export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
133
159
|
const now = Date.now();
|
|
134
160
|
const cutoff = now - WORKSPACE_RETENTION_MS;
|
|
@@ -147,6 +173,7 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
|
147
173
|
const escalatedIds: string[] = [];
|
|
148
174
|
const reassignedIds: string[] = [];
|
|
149
175
|
const escalationTargets: string[] = [];
|
|
176
|
+
const reconciledLandedIds: string[] = [];
|
|
150
177
|
const escalationMs = stewardEscalationMs();
|
|
151
178
|
const fallbackTarget = stewardFallbackTarget();
|
|
152
179
|
for (const ws of all) {
|
|
@@ -159,6 +186,20 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
|
159
186
|
const stewardOnline = Boolean(steward && getAgent(steward) && getAgent(steward)!.status !== "offline");
|
|
160
187
|
const strandedAt = typeof meta.strandedAt === "number" ? meta.strandedAt : undefined;
|
|
161
188
|
|
|
189
|
+
if (!stewardOnline) {
|
|
190
|
+
const orch = workspaceOwnerOrchestrator(fresh, listOrchestrators().filter((o) => o.status === "online" && o.apiUrl), { requireApiUrl: true });
|
|
191
|
+
const preview = orch?.apiUrl ? await fetchHostMergePreview(orch.apiUrl, fresh, { checkPr: true }) : null;
|
|
192
|
+
if (preview && !("available" in preview) && !preview.error && !preview.missing) {
|
|
193
|
+
const landed = previewProvesLanded(preview);
|
|
194
|
+
if (landed) {
|
|
195
|
+
reconcileLandedWorkspace(fresh, preview);
|
|
196
|
+
patchWorkspaceMetadata(fresh.id, { strandedAt: undefined, escalatedAt: undefined, strandedNotifiedSteward: undefined });
|
|
197
|
+
reconciledLandedIds.push(fresh.id);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
162
203
|
if (stewardOnline) {
|
|
163
204
|
if (strandedAt !== undefined && meta.strandedNotifiedSteward !== steward) {
|
|
164
205
|
const sent = notifyTarget(
|
|
@@ -206,30 +247,45 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
|
206
247
|
}
|
|
207
248
|
|
|
208
249
|
const abandonedIds: string[] = [];
|
|
250
|
+
const autoAbandonDeferred: string[] = [];
|
|
209
251
|
const notifiedStewards: string[] = [];
|
|
252
|
+
const reviewOrchestrators = listOrchestrators();
|
|
210
253
|
for (const ws of all) {
|
|
211
254
|
if (ws.status === "review_requested" && ws.updatedAt < reviewCutoff) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
255
|
+
const fresh = getWorkspace(ws.id);
|
|
256
|
+
if (!fresh || fresh.status !== "review_requested") continue;
|
|
257
|
+
const decision = await reviewAutoAbandonDecision(fresh, reviewOrchestrators);
|
|
258
|
+
if (decision.kind === "defer") {
|
|
259
|
+
autoAbandonDeferred.push(`${fresh.id}:${decision.reason}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (decision.kind === "landed") {
|
|
263
|
+
reconcileLandedWorkspace(fresh, decision.preview);
|
|
264
|
+
reconciledLandedIds.push(fresh.id);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
updateWorkspaceStatus(fresh.id, "abandoned", { autoAbandoned: true, abandonedReason: "review_requested TTL exceeded", abandonedAt: now });
|
|
269
|
+
abandonedIds.push(fresh.id);
|
|
270
|
+
const target = fresh.stewardAgentId ?? fallbackTarget;
|
|
215
271
|
const sent = notifyTarget(
|
|
216
272
|
"workspace.auto_abandoned",
|
|
217
|
-
{ workspaceId:
|
|
273
|
+
{ workspaceId: fresh.id, repoRoot: fresh.repoRoot },
|
|
218
274
|
target,
|
|
219
275
|
"Workspace auto-abandoned",
|
|
220
|
-
`Workspace \`${
|
|
221
|
-
{ kind: "workspace.auto-abandoned", workspaceId:
|
|
276
|
+
`Workspace \`${fresh.branch ?? fresh.id}\` in ${fresh.repoRoot} was auto-abandoned after ${Math.round(WORKSPACE_REVIEW_TTL_MS / DAY_MS)}d without steward action. Run workspace cleanup to reclaim the worktree.`,
|
|
277
|
+
{ kind: "workspace.auto-abandoned", workspaceId: fresh.id, repoRoot: fresh.repoRoot, branch: fresh.branch },
|
|
222
278
|
);
|
|
223
279
|
if (sent) notifiedStewards.push(sent);
|
|
224
280
|
createActivityEvent({
|
|
225
|
-
clientId: `workspace-gc-abandon-${
|
|
281
|
+
clientId: `workspace-gc-abandon-${fresh.id}-${now}`,
|
|
226
282
|
kind: "state",
|
|
227
283
|
title: "Workspace auto-abandoned",
|
|
228
|
-
body: `${
|
|
229
|
-
meta:
|
|
284
|
+
body: `${fresh.branch ?? fresh.id} in ${fresh.repoRoot} - review_requested for ${Math.round((now - fresh.updatedAt) / DAY_MS)}d`,
|
|
285
|
+
meta: fresh.branch ?? fresh.id,
|
|
230
286
|
icon: "ti-clock-x",
|
|
231
287
|
view: "orchestrators",
|
|
232
|
-
metadata: { source: "server", maintenanceJobId: "workspace-gc", workspaceId:
|
|
288
|
+
metadata: { source: "server", maintenanceJobId: "workspace-gc", workspaceId: fresh.id },
|
|
233
289
|
});
|
|
234
290
|
}
|
|
235
291
|
}
|
|
@@ -255,12 +311,14 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
|
255
311
|
return {
|
|
256
312
|
prunedTerminal: terminalIds,
|
|
257
313
|
autoAbandoned: abandonedIds,
|
|
314
|
+
autoAbandonDeferred,
|
|
258
315
|
notifiedStewards,
|
|
259
316
|
pruneCommands,
|
|
260
317
|
releasedLeaseRepos,
|
|
261
318
|
escalated: escalatedIds,
|
|
262
319
|
reassigned: reassignedIds,
|
|
263
320
|
escalationTargets,
|
|
321
|
+
reconciledLanded: reconciledLandedIds,
|
|
264
322
|
};
|
|
265
323
|
}
|
|
266
324
|
|
|
@@ -288,7 +346,7 @@ export async function refreshIdleWorkspaces(): Promise<Record<string, unknown>>
|
|
|
288
346
|
const now = Date.now();
|
|
289
347
|
|
|
290
348
|
for (const ws of candidates) {
|
|
291
|
-
const orch =
|
|
349
|
+
const orch = workspaceOwnerOrchestrator(ws, orchestrators);
|
|
292
350
|
if (!orch?.agentId) { skipped.push(`${ws.id}:no-orchestrator`); continue; }
|
|
293
351
|
|
|
294
352
|
// Gate: owner must exist and not be busy.
|
package/src/routes/commands.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { handleReviewerMergeResult } from "../reviewer-pipeline";
|
|
|
22
22
|
import { CLEAR_MERGE_NO_PROGRESS, noteMergeNoProgress, scheduleRepoMerge } from "../workspace-auto-merge";
|
|
23
23
|
import { enqueueContinuationInjectionAfterSelfResume } from "../self-resume";
|
|
24
24
|
import { type Command, type CommandStatus, type CreateCommandInput, type WorkspaceStatus } from "../types";
|
|
25
|
+
import { commandTargetsWorkspaceOwner } from "../workspace-host";
|
|
25
26
|
|
|
26
27
|
const VALID_COMMAND_STATUSES = ["pending", "accepted", "running", "succeeded", "failed", "timed_out", "rejected", "canceled"] as const;
|
|
27
28
|
|
|
@@ -227,10 +228,13 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
227
228
|
if (command.type === "workspace.cleanup" && command.status === "succeeded" && isRecord(command.result)) {
|
|
228
229
|
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
229
230
|
if (workspaceId) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
const workspace = getWorkspace(workspaceId);
|
|
232
|
+
if (workspace && commandTargetsWorkspaceOwner(command, workspace)) {
|
|
233
|
+
updateWorkspaceStatus(workspaceId, "cleaned", {
|
|
234
|
+
cleanupResult: command.result,
|
|
235
|
+
cleanupCommandId: command.id,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
if (command.type === "workspace.idle-refresh" && command.status === "succeeded" && isRecord(command.result)) {
|
|
@@ -261,6 +265,10 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
261
265
|
// Snapshot the row BEFORE the recycle repoints `branch` (#206) — the landed
|
|
262
266
|
// branch name + author (#239 branch.landed push) come from this pre-mutation state.
|
|
263
267
|
const landedWorkspace = getWorkspace(workspaceId);
|
|
268
|
+
if (landedWorkspace && !commandTargetsWorkspaceOwner(command, landedWorkspace)) {
|
|
269
|
+
auditCommandOutcome(command);
|
|
270
|
+
return json(command);
|
|
271
|
+
}
|
|
264
272
|
const newBranch = cleanString(command.result.newBranch, "result.newBranch", { max: 240 });
|
|
265
273
|
const normalizedStatus = command.result.merged === true
|
|
266
274
|
&& !newBranch
|
|
@@ -361,6 +369,10 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
361
369
|
// auto-merge job backs off after N instead of busy-looping (#638).
|
|
362
370
|
const current = getWorkspace(command.correlationId);
|
|
363
371
|
if (current && current.status === "merge_planned") {
|
|
372
|
+
if (!commandTargetsWorkspaceOwner(command, current)) {
|
|
373
|
+
auditCommandOutcome(command);
|
|
374
|
+
return json(command);
|
|
375
|
+
}
|
|
364
376
|
const failedResultStatus = isRecord(command.result)
|
|
365
377
|
? optionalEnum(command.result.status, "result.status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined
|
|
366
378
|
: undefined;
|
|
@@ -420,6 +432,10 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
420
432
|
// a status a human/agent has since moved on (merge_planned, abandoned, …).
|
|
421
433
|
const current = getWorkspace(workspaceId);
|
|
422
434
|
if (current && (current.status === "active" || current.status === "ready")) {
|
|
435
|
+
if (!commandTargetsWorkspaceOwner(command, current)) {
|
|
436
|
+
auditCommandOutcome(command);
|
|
437
|
+
return json(command);
|
|
438
|
+
}
|
|
423
439
|
if (removed) {
|
|
424
440
|
// The owner exited and the worktree had no work — the orchestrator
|
|
425
441
|
// already deleted it on disk. Drop the DB row immediately instead of
|