agent-relay-server 0.119.7 → 0.119.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-Azda2GIf.js → MessageBubble-U5sX3SNZ.js} +2 -2
  4. package/public/assets/{MessageBubble-Azda2GIf.js.map → MessageBubble-U5sX3SNZ.js.map} +1 -1
  5. package/public/assets/PlanGraphPanel-Bi2gvXeE.js +2 -0
  6. package/public/assets/PlanGraphPanel-Bi2gvXeE.js.map +1 -0
  7. package/public/assets/{activity-B0hz4XPy.js → activity-Cwkz51pG.js} +2 -2
  8. package/public/assets/{activity-B0hz4XPy.js.map → activity-Cwkz51pG.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BF0oSOTq.js → agent-profiles-BgddVqRp.js} +2 -2
  10. package/public/assets/{agent-profiles-BF0oSOTq.js.map → agent-profiles-BgddVqRp.js.map} +1 -1
  11. package/public/assets/{agents-CW4--j3Q.js → agents-CiGYBw0h.js} +2 -2
  12. package/public/assets/{agents-CW4--j3Q.js.map → agents-CiGYBw0h.js.map} +1 -1
  13. package/public/assets/{automation-S6_bQ2t-.js → automation-BBJB4RaE.js} +2 -2
  14. package/public/assets/{automation-S6_bQ2t-.js.map → automation-BBJB4RaE.js.map} +1 -1
  15. package/public/assets/{chat-hoFhhvgy.js → chat-DS578gZy.js} +2 -2
  16. package/public/assets/{chat-hoFhhvgy.js.map → chat-DS578gZy.js.map} +1 -1
  17. package/public/assets/{formatted-body-BHm145gP.js → formatted-body-DYUVqWym.js} +3 -3
  18. package/public/assets/{formatted-body-BHm145gP.js.map → formatted-body-DYUVqWym.js.map} +1 -1
  19. package/public/assets/{formatted-body-impl-CvE31j9F.js → formatted-body-impl-DdcyxVO0.js} +2 -2
  20. package/public/assets/{formatted-body-impl-CvE31j9F.js.map → formatted-body-impl-DdcyxVO0.js.map} +1 -1
  21. package/public/assets/{index-TamtghSr.js → index-CqcAqQKW.js} +6 -6
  22. package/public/assets/{index-TamtghSr.js.map → index-CqcAqQKW.js.map} +1 -1
  23. package/public/assets/{insights-Ci9ftVfl.js → insights-BunHbsTA.js} +2 -2
  24. package/public/assets/{insights-Ci9ftVfl.js.map → insights-BunHbsTA.js.map} +1 -1
  25. package/public/assets/{maintenance-bVhNbr33.js → maintenance-cICBVcpJ.js} +2 -2
  26. package/public/assets/{maintenance-bVhNbr33.js.map → maintenance-cICBVcpJ.js.map} +1 -1
  27. package/public/assets/{managed-agents-D5HO4dhI.js → managed-agents-BK9_Gi-M.js} +2 -2
  28. package/public/assets/{managed-agents-D5HO4dhI.js.map → managed-agents-BK9_Gi-M.js.map} +1 -1
  29. package/public/assets/{markdown-preview-impl-DO-p83dz.js → markdown-preview-impl-EZfcTHG9.js} +2 -2
  30. package/public/assets/{markdown-preview-impl-DO-p83dz.js.map → markdown-preview-impl-EZfcTHG9.js.map} +1 -1
  31. package/public/assets/{memory-D72o8umQ.js → memory-Vi-8HvK3.js} +2 -2
  32. package/public/assets/{memory-D72o8umQ.js.map → memory-Vi-8HvK3.js.map} +1 -1
  33. package/public/assets/{messages-5L_Bc_9w.js → messages-DScIiBMI.js} +2 -2
  34. package/public/assets/{messages-5L_Bc_9w.js.map → messages-DScIiBMI.js.map} +1 -1
  35. package/public/assets/{orchestrators-CnfgkQpl.js → orchestrators-DLogldXx.js} +2 -2
  36. package/public/assets/{orchestrators-CnfgkQpl.js.map → orchestrators-DLogldXx.js.map} +1 -1
  37. package/public/assets/{overview-Bu39NIyR.js → overview-B_RQAQR8.js} +2 -2
  38. package/public/assets/{overview-Bu39NIyR.js.map → overview-B_RQAQR8.js.map} +1 -1
  39. package/public/assets/{pairs-WuamP5W5.js → pairs-CSiPQ7E2.js} +2 -2
  40. package/public/assets/{pairs-WuamP5W5.js.map → pairs-CSiPQ7E2.js.map} +1 -1
  41. package/public/assets/{prompt-settings-BCrQreU6.js → prompt-settings-DsDi_Xpd.js} +2 -2
  42. package/public/assets/{prompt-settings-BCrQreU6.js.map → prompt-settings-DsDi_Xpd.js.map} +1 -1
  43. package/public/assets/{registry-kHKn0xwg.js → registry-DHFUWIdb.js} +2 -2
  44. package/public/assets/{registry-kHKn0xwg.js.map → registry-DHFUWIdb.js.map} +1 -1
  45. package/public/assets/{security-Dk5t3r_K.js → security-D2nHkJH6.js} +2 -2
  46. package/public/assets/{security-Dk5t3r_K.js.map → security-D2nHkJH6.js.map} +1 -1
  47. package/public/assets/{select-BdsZQfU2.js → select-BjNcommA.js} +2 -2
  48. package/public/assets/{select-BdsZQfU2.js.map → select-BjNcommA.js.map} +1 -1
  49. package/public/assets/{settings-EpjzQHnZ.js → settings-DiUEq54n.js} +2 -2
  50. package/public/assets/{settings-EpjzQHnZ.js.map → settings-DiUEq54n.js.map} +1 -1
  51. package/public/assets/{tasks-CedMQHbg.js → tasks-Dx8GVQiR.js} +2 -2
  52. package/public/assets/{tasks-CedMQHbg.js.map → tasks-Dx8GVQiR.js.map} +1 -1
  53. package/public/assets/{teams-BlHBo5Qt.js → teams-DQtf7bha.js} +2 -2
  54. package/public/assets/{teams-BlHBo5Qt.js.map → teams-DQtf7bha.js.map} +1 -1
  55. package/public/assets/{terminal-viewer-impl-gGqYpsjq.js → terminal-viewer-impl-D5rhXV4u.js} +2 -2
  56. package/public/assets/{terminal-viewer-impl-gGqYpsjq.js.map → terminal-viewer-impl-D5rhXV4u.js.map} +1 -1
  57. package/public/assets/types-uVOXgvMT.js.map +1 -1
  58. package/public/assets/{work-queue-D7opMqtU.js → work-queue-BfWNDp7U.js} +2 -2
  59. package/public/assets/{work-queue-D7opMqtU.js.map → work-queue-BfWNDp7U.js.map} +1 -1
  60. package/public/index.html +2 -2
  61. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  62. package/src/db/index.ts +1 -0
  63. package/src/db/migrations.ts +7 -0
  64. package/src/db/orchestrators.ts +1 -1
  65. package/src/db/provider-quotas.ts +2 -34
  66. package/src/db/schema.ts +2 -0
  67. package/src/db/work-evidence.ts +38 -0
  68. package/src/db/workspaces.ts +7 -3
  69. package/src/lifecycle-manager.ts +2 -4
  70. package/src/maintenance/jobs.ts +10 -1
  71. package/src/maintenance/workspaces.ts +71 -13
  72. package/src/routes/commands.ts +20 -4
  73. package/src/routes/workspaces.ts +4 -7
  74. package/src/services/transient-agent-reaper.ts +54 -9
  75. package/src/workspace-actions.ts +3 -4
  76. package/src/workspace-auto-merge.ts +95 -13
  77. package/src/workspace-host.ts +55 -0
  78. package/src/workspace-merge.ts +3 -4
  79. package/src/workspace-orphans.ts +78 -55
  80. package/src/workspace-probe.ts +2 -3
  81. package/public/assets/PlanGraphPanel-B9TN_KQr.js +0 -2
  82. 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-TamtghSr.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-D7opMqtU.js.map
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":"wXAWM,EAAwC,CAC5C,KAAM,kDACN,QAAS,wDACT,QAAS,2DACT,YAAa,wDACd,CAED,SAAgB,GAAgB,CAC9B,IAAM,EAAM,GAAQ,CACd,EAAa,EAAe,GAAM,EAAE,WAAW,CAC/C,EAAU,EAAe,GAAM,EAAE,QAAQ,CACzC,EAAM,EAAe,GAAM,EAAE,IAAI,CACjC,EAAc,EAAe,GAAM,EAAE,YAAY,CACjD,EAAU,EAAe,GAAM,EAAE,QAAQ,CACzC,EAAiB,EAAe,GAAM,EAAE,eAAe,CACvD,EAAY,EAAe,GAAM,EAAE,UAAU,CAC7C,EAAc,EAAe,GAAM,EAAE,YAAY,CACjD,EAAkB,EAAe,GAAM,EAAE,gBAAgB,CACzD,EAAqB,EAAe,GAAM,EAAE,mBAAmB,CAE/D,EAAgB,GAAkB,CAClC,EAAQ,GAAmB,CAE3B,EAAiB,EAAM,OAAQ,GAAM,EAAE,UAAU,CAAC,OAExD,SAAS,EAAY,EAAqB,CACxC,GAAI,CAAC,EAAQ,KAAM,CAAE,EAAU,aAAc,mCAAmC,CAAE,OAC9E,EAAK,aAAe,QAAU,EAAK,KAAM,EAAY,EAAK,KAAK,GAAG,CAC7D,EAAK,aAAe,WAAa,EAAK,SAAS,EAAQ,EAAK,QAAQ,GAAG,CAGlF,SAAS,EAAa,EAAqB,CAEzC,EAAY,cAAe,WADb,EAAK,QAAU,EAAK,aAAe,OAAS,IAAI,EAAK,KAAO,YAAY,EAAK,MAC/C,+BAAkC,CACxE,EAAK,aAAe,QAAU,EAAK,KAAM,EAAmB,EAAK,KAAM,WAAW,CAC7E,EAAK,aAAe,WAAa,EAAK,SAAS,EAAgB,EAAK,QAAQ,GAAG,EACxF,CAGJ,SAAS,EAAa,EAAqB,CACrC,EAAK,MAAM,EAAe,EAAK,KAAK,CAG1C,OACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,qBAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,6DAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,mCAAf,EACE,EAAA,EAAA,KAAC,EAAD,CAAY,UAAU,gCAAkC,CAAA,EACxD,EAAA,EAAA,KAAC,KAAD,CAAI,UAAU,iCAAwB,aAAe,CAAA,CACpD,EAAiB,IAChB,EAAA,EAAA,MAAC,EAAD,CAAO,UAAU,wEAAjB,CACG,EAAe,aACV,GAEN,IACN,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,mCAAf,EACE,EAAA,EAAA,KAAC,QAAD,CAAO,UAAU,kDAAyC,WAAgB,CAAA,EAC1E,EAAA,EAAA,MAAC,SAAD,CACE,UAAU,mEACV,MAAO,EAAQ,KACf,SAAW,GAAM,EAAI,CAAE,QAAS,CAAE,GAAG,EAAS,KAAM,EAAE,OAAO,MAAO,CAAE,CAAC,UAHzE,EAKE,EAAA,EAAA,KAAC,SAAD,CAAQ,MAAM,YAAG,kBAAwB,CAAA,CACxC,EAAc,IAAK,IAAM,EAAA,EAAA,KAAC,SAAD,CAAmB,MAAO,EAAE,YAAK,EAAY,EAAE,CAAU,CAA5C,EAAE,GAA0C,CAAC,CAC7E,GACL,GACF,IAEN,EAAA,EAAA,KAAC,EAAD,CAAY,UAAU,mCACpB,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,0BAAf,CACG,EAAM,SAAW,IAChB,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,2DAAkD,sBAAyB,CAAA,CAE3F,EAAM,IAAK,IACV,EAAA,EAAA,KAAC,EAAD,CAEQ,OACD,MACL,UAAY,GAAO,EAAW,GAAM,EAAY,EAAW,GAAI,CAAG,EAAG,MAAM,IAAI,CAC/E,YAAe,EAAY,EAAK,CAChC,aAAgB,EAAa,EAAK,CAClC,SAAU,EAAK,SAAa,EAAa,EAAK,CAAG,IAAA,GACjD,CAPK,EAAK,GAOV,CACF,CACE,GACK,CAAA,CACT,GAIV,SAAS,EAAc,CACrB,OAAM,MAAK,YAAW,UAAS,WAAU,YAQxC,CACD,IAAM,EAAgB,EAAgB,EAAK,WAAa,EAAgB,KAClE,EAAc,EAAc,EAAK,SAAW,kDAElD,OACE,EAAA,EAAA,KAAC,EAAD,CAAM,UAAW,EAAK,UAAY,4BAA8B,aAC9D,EAAA,EAAA,KAAC,EAAD,CAAa,UAAU,gBACrB,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,kCAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,0BAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,kDAAf,EACE,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,wCAAgC,EAAK,MAAa,CAAA,EAClE,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAW,8BAA8B,aAAkB,EAAK,SAAiB,CAAA,EAC1G,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAW,8BAA8B,aAAgB,EAAK,OAAe,CAAA,CACrG,EAAK,aAAe,YACnB,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAU,uEAA8D,MAAW,CAAA,CAE1G,GACL,EAAK,OACJ,EAAA,EAAA,KAAC,IAAD,CAAG,UAAU,2DAAmD,EAAK,KAAS,CAAA,EAEhF,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,2EAAf,CACG,EAAK,QACJ,EAAA,EAAA,MAAC,OAAD,CAAM,MAAM,iBAAZ,EACE,EAAA,EAAA,KAAC,EAAD,CAAc,UAAU,sBAAwB,CAAA,CAAC,EAAU,EAAK,MAAM,CACjE,GAER,EAAK,SACJ,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,qBAAa,EAAK,OAAc,CAAA,EAElD,EAAA,EAAA,MAAC,OAAD,CAAM,MAAO,OAAO,EAAK,UAAU,UAAnC,EACE,EAAA,EAAA,KAAC,EAAD,CAAU,UAAU,sBAAwB,CAAA,CAAC,EAAQ,EAAK,EAAK,UAAU,CACpE,GACH,GACF,IACN,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,wCAAf,CACG,EAAK,YACJ,EAAA,EAAA,MAAC,EAAD,CAAQ,KAAK,KAAK,UAAU,2DAA2D,QAAS,WAAhG,EACE,EAAA,EAAA,KAAC,EAAD,CAAc,UAAU,mBAAqB,CAAA,CAAA,SACtC,GAEV,IACC,EAAA,EAAA,KAAC,EAAD,CAAQ,KAAK,KAAK,QAAQ,UAAU,UAAU,cAAc,QAAS,WAAU,SAEtE,CAAA,EAEX,EAAA,EAAA,MAAC,EAAD,CAAQ,KAAK,KAAK,QAAQ,UAAU,UAAU,oFAAoF,QAAS,WAA3I,EACE,EAAA,EAAA,KAAC,EAAD,CAAG,UAAU,mBAAqB,CAAA,CAAA,UAC3B,GACL,GACF,GACM,CAAA,CACT,CAAA"}
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-TamtghSr.js"></script>
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-BHm145gP.js">
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">
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.119.7",
4
+ "version": "0.119.9",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
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";
@@ -218,6 +218,13 @@ 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")) {
@@ -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 → apply the regression guard (rejects improbable utilization drops from noisy
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
- // Same-priority sources: prefer the newer reading. Keep the regression guard for noisy
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
@@ -585,6 +585,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
585
585
  mode TEXT NOT NULL,
586
586
  requested_mode TEXT,
587
587
  status TEXT NOT NULL DEFAULT 'active',
588
+ orchestrator_id TEXT,
588
589
  owner_agent_id TEXT,
589
590
  owner_policy_name TEXT,
590
591
  owner_automation_run_id TEXT,
@@ -596,6 +597,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
596
597
  cleaned_at INTEGER
597
598
  );
598
599
  CREATE INDEX IF NOT EXISTS idx_workspaces_repo_status ON workspaces(repo_root, status);
600
+ CREATE INDEX IF NOT EXISTS idx_workspaces_orchestrator ON workspaces(orchestrator_id);
599
601
  CREATE INDEX IF NOT EXISTS idx_workspaces_owner_agent ON workspaces(owner_agent_id);
600
602
  CREATE INDEX IF NOT EXISTS idx_workspaces_policy ON workspaces(owner_policy_name);
601
603
 
@@ -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
+ }
@@ -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,
@@ -1,5 +1,5 @@
1
1
  import { createCommand } from "./commands-db";
2
- import { isPathWithinBase } from "./utils";
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.find(
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",
@@ -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 = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
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
- updateWorkspaceStatus(ws.id, "abandoned", { autoAbandoned: true, abandonedReason: "review_requested TTL exceeded", abandonedAt: now });
213
- abandonedIds.push(ws.id);
214
- const target = ws.stewardAgentId ?? fallbackTarget;
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: ws.id, repoRoot: ws.repoRoot },
273
+ { workspaceId: fresh.id, repoRoot: fresh.repoRoot },
218
274
  target,
219
275
  "Workspace auto-abandoned",
220
- `Workspace \`${ws.branch ?? ws.id}\` in ${ws.repoRoot} was auto-abandoned after ${Math.round(WORKSPACE_REVIEW_TTL_MS / DAY_MS)}d without steward action. Run workspace cleanup to reclaim the worktree.`,
221
- { kind: "workspace.auto-abandoned", workspaceId: ws.id, repoRoot: ws.repoRoot, branch: ws.branch },
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-${ws.id}-${now}`,
281
+ clientId: `workspace-gc-abandon-${fresh.id}-${now}`,
226
282
  kind: "state",
227
283
  title: "Workspace auto-abandoned",
228
- body: `${ws.branch ?? ws.id} in ${ws.repoRoot} - review_requested for ${Math.round((now - ws.updatedAt) / DAY_MS)}d`,
229
- meta: ws.branch ?? ws.id,
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: ws.id },
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 = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
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.
@@ -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
- updateWorkspaceStatus(workspaceId, "cleaned", {
231
- cleanupResult: command.result,
232
- cleanupCommandId: command.id,
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
@@ -15,6 +15,7 @@ import { AUTO_MERGE_POLICIES } from "../workspace-merge";
15
15
  import { workspaceActiveClaim } from "../workspace-claim";
16
16
  import { resolveCoordinatorAgentId } from "../steward-identity";
17
17
  import { relayToken } from "../config";
18
+ import { workspaceOwnerOrchestrator } from "../workspace-host";
18
19
 
19
20
  export const getWorkspaces: Handler = (req) => {
20
21
  try {
@@ -44,9 +45,7 @@ async function proxyWorkspaceHostGet(workspaceId: string, hostPath: string, extr
44
45
  if (workspace.mode !== "isolated" || !workspace.worktreePath) {
45
46
  return json({ available: false, reason: "no isolated worktree" });
46
47
  }
47
- const orch = listOrchestrators().find(
48
- (candidate) => candidate.status === "online" && candidate.apiUrl && isPathWithinBase(workspace.sourceCwd, candidate.baseDir),
49
- );
48
+ const orch = workspaceOwnerOrchestrator(workspace, listOrchestrators(), { requireApiUrl: true });
50
49
  if (!orch?.apiUrl) return json({ available: false, reason: "owning orchestrator offline" });
51
50
  const query = new URLSearchParams({ path: workspace.worktreePath });
52
51
  if (workspace.baseRef) query.set("baseRef", workspace.baseRef);
@@ -287,9 +286,7 @@ export const postWorkspaceOrphanReclaim: Handler = async (req) => {
287
286
 
288
287
  async function fetchWorkspaceGitState(workspace: WorkspaceRecord): Promise<{ state: WorkspaceGitState } | { unavailable: string }> {
289
288
  if (workspace.mode !== "isolated" || !workspace.worktreePath) return { unavailable: "no isolated worktree" };
290
- const orch = listOrchestrators().find(
291
- (candidate) => candidate.status === "online" && candidate.apiUrl && isPathWithinBase(workspace.sourceCwd, candidate.baseDir),
292
- );
289
+ const orch = workspaceOwnerOrchestrator(workspace, listOrchestrators(), { requireApiUrl: true });
293
290
  if (!orch?.apiUrl) return { unavailable: "owning orchestrator offline" };
294
291
  const query = new URLSearchParams({ path: workspace.worktreePath });
295
292
  if (workspace.baseRef) query.set("baseRef", workspace.baseRef);
@@ -342,7 +339,7 @@ export const getWorkspaceDiagnostics: Handler = async (_req, params) => {
342
339
  // #640 — surface the branch agent's coordinator (owner's `spawnedBy` lineage) so a steward
343
340
  // inspecting the workspace knows who to escalate to, not just `repoRoot`.
344
341
  const coordinatorAgentId = resolveCoordinatorAgentId(workspace.ownerAgentId);
345
- const orch = listOrchestrators().find((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
342
+ const orch = workspaceOwnerOrchestrator(workspace, listOrchestrators(), { onlineOnly: false });
346
343
  const orchOnline = Boolean(orch) && orch!.status === "online";
347
344
  const fetched = await fetchWorkspaceGitState(workspace);
348
345
  const gitState = "state" in fetched ? fetched.state : undefined;