agent-relay-server 0.119.6 → 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 (85) 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/commands-db.ts +72 -20
  63. package/src/db/index.ts +1 -0
  64. package/src/db/migrations.ts +7 -0
  65. package/src/db/orchestrators.ts +1 -1
  66. package/src/db/provider-quotas.ts +2 -34
  67. package/src/db/schema.ts +2 -0
  68. package/src/db/work-evidence.ts +38 -0
  69. package/src/db/workspaces.ts +48 -3
  70. package/src/lifecycle-manager.ts +2 -4
  71. package/src/maintenance/jobs.ts +10 -1
  72. package/src/maintenance/workspaces.ts +71 -13
  73. package/src/routes/commands.ts +20 -4
  74. package/src/routes/workspaces.ts +6 -11
  75. package/src/services/shutdown-agent.ts +6 -1
  76. package/src/services/spawn-agent.ts +50 -7
  77. package/src/services/transient-agent-reaper.ts +82 -23
  78. package/src/workspace-actions.ts +26 -16
  79. package/src/workspace-auto-merge.ts +95 -13
  80. package/src/workspace-host.ts +55 -0
  81. package/src/workspace-merge.ts +3 -4
  82. package/src/workspace-orphans.ts +78 -55
  83. package/src/workspace-probe.ts +2 -3
  84. package/public/assets/PlanGraphPanel-B9TN_KQr.js +0 -2
  85. 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":"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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-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.6",
4
+ "version": "0.119.9",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -26,19 +26,25 @@ interface CommandFilters {
26
26
  limit?: number;
27
27
  }
28
28
 
29
- const ACTIVE_STATUSES: CommandStatus[] = ["pending", "accepted", "running"];
29
+ const CLAIMED_ACTIVE_STATUSES: CommandStatus[] = ["accepted", "running"];
30
+ const SPAWN_IN_FLIGHT_STATUSES: CommandStatus[] = ["pending", "accepted", "running"];
30
31
  const TERMINAL_STATUSES: CommandStatus[] = ["succeeded", "failed", "canceled", "timed_out"];
31
32
 
32
- // #930 — an `agent.spawn` command carries a hard TTL from CREATION (see
33
- // defaultExpiresAt). The #880 concurrency cap introduces REAL queuing, so a
34
- // legitimately-queued spawn can sit `accepted`/`running` in the orchestrator past
35
- // that create-time TTL and be swept to `timed_out` while the orchestrator still
36
- // intends to run it. Refresh the TTL from dispatch-intent (claim → accepted, or
37
- // slot-acquire running) so the window measures from when the host committed to
38
- // the spawn, not from enqueue. Scoped to `agent.spawn`; every other command type
39
- // keeps create-time TTL semantics.
33
+ // #930/#953command TTLs are execution leases, not delivery deadlines. The #880
34
+ // spawn cap and long inline orchestrator commands both introduce legitimate
35
+ // queuing before dispatch, so pending commands must not be swept before the host
36
+ // can claim them. Refresh expiring command types at dispatch-intent (claim
37
+ // accepted, or slot-acquire running) so the window measures from when the host
38
+ // committed to doing the work, not from enqueue.
40
39
  const SPAWN_TTL_MS = 180_000;
41
- const SPAWN_DISPATCH_STATUSES: CommandStatus[] = ["accepted", "running"];
40
+ const LIFECYCLE_TTL_MS = 30_000;
41
+ const DISPATCH_STATUSES: CommandStatus[] = ["accepted", "running"];
42
+
43
+ function commandTtlMs(type: string): number | undefined {
44
+ if (type === "agent.spawn") return SPAWN_TTL_MS;
45
+ if (type === "agent.shutdown" || type === "agent.restart" || type === "agent.reconnect" || type === "agent.kill" || type === "agent.compact" || type === "agent.clearContext" || type === "agent.injectContext") return LIFECYCLE_TTL_MS;
46
+ return undefined;
47
+ }
42
48
 
43
49
  function isTerminalStatus(status: CommandStatus): boolean {
44
50
  return TERMINAL_STATUSES.includes(status);
@@ -123,6 +129,51 @@ export function listCommands(filters: CommandFilters = {}): Command[] {
123
129
  return rows.map(rowToCommand);
124
130
  }
125
131
 
132
+ export function findSpawnCommandByRequestId(spawnRequestId: string, statuses?: CommandStatus[]): Command | null {
133
+ const cleaned = spawnRequestId.trim();
134
+ if (!cleaned) return null;
135
+ const statusClause = statuses?.length
136
+ ? `AND status IN (${statuses.map(() => "?").join(",")})`
137
+ : "";
138
+ const rows = getDb()
139
+ .query(`
140
+ SELECT * FROM commands
141
+ WHERE type = 'agent.spawn'
142
+ ${statusClause}
143
+ AND (correlation_id = ? OR json_extract(params, '$.spawnRequestId') = ?)
144
+ ORDER BY created_at DESC
145
+ LIMIT 1
146
+ `)
147
+ .all(...(statuses ?? []), cleaned, cleaned) as CommandRow[];
148
+ return rows[0] ? rowToCommand(rows[0]) : null;
149
+ }
150
+
151
+ export function countInFlightSpawnCommands(parentId: string): number {
152
+ const row = getDb()
153
+ .query(
154
+ `SELECT count(*) AS n
155
+ FROM commands
156
+ WHERE type = 'agent.spawn'
157
+ AND status IN (${SPAWN_IN_FLIGHT_STATUSES.map(() => "?").join(",")})
158
+ AND json_extract(params, '$.spawnedBy') = ?`,
159
+ )
160
+ .get(...SPAWN_IN_FLIGHT_STATUSES, parentId) as { n: number };
161
+ return row.n;
162
+ }
163
+
164
+ export function createSpawnCommandWithinQuota(
165
+ input: CreateCommandInput,
166
+ quota: { parentId: string; maxSpawnedAgents: number; liveChildren: () => number },
167
+ ): { command: Command | null; occupiedSlots: number } {
168
+ return getDb().transaction(() => {
169
+ const live = quota.liveChildren();
170
+ const inFlight = countInFlightSpawnCommands(quota.parentId);
171
+ const occupied = live + inFlight;
172
+ if (occupied >= quota.maxSpawnedAgents) return { command: null, occupiedSlots: occupied };
173
+ return { command: createCommand(input), occupiedSlots: occupied + 1 };
174
+ })();
175
+ }
176
+
126
177
  export function updateCommand(id: string, input: UpdateCommandInput): Command | null {
127
178
  const existing = getCommand(id);
128
179
  if (!existing) return null;
@@ -145,12 +196,14 @@ export function updateCommand(id: string, input: UpdateCommandInput): Command |
145
196
  return existing;
146
197
  }
147
198
  const now = Date.now();
148
- // #930 — refresh the spawn TTL from dispatch-intent when the command is claimed or
149
- // dispatched (see SPAWN_DISPATCH_STATUSES). Other command types keep their existing
150
- // expiry untouched.
199
+ // #930/#953 — refresh the lease from dispatch-intent when an expiring command is
200
+ // claimed or starts running. Pending commands are not swept before delivery, so a
201
+ // busy orchestrator must not inherit an already-expired lease when it finally gets
202
+ // back to the command loop.
203
+ const ttlMs = input.status && DISPATCH_STATUSES.includes(input.status) ? commandTtlMs(existing.type) : undefined;
151
204
  const nextExpiresAt =
152
- existing.type === "agent.spawn" && input.status && SPAWN_DISPATCH_STATUSES.includes(input.status)
153
- ? now + SPAWN_TTL_MS
205
+ ttlMs !== undefined
206
+ ? now + ttlMs
154
207
  : existing.expiresAt ?? null;
155
208
  getDb().query(`
156
209
  UPDATE commands
@@ -179,16 +232,15 @@ export function deleteCommand(id: string): boolean {
179
232
 
180
233
  export function expireCommands(now: number = Date.now()): Command[] {
181
234
  const rows = getDb()
182
- .query(`SELECT * FROM commands WHERE expires_at IS NOT NULL AND expires_at <= ? AND status IN (${ACTIVE_STATUSES.map(() => "?").join(",")})`)
183
- .all(now, ...ACTIVE_STATUSES) as CommandRow[];
235
+ .query(`SELECT * FROM commands WHERE expires_at IS NOT NULL AND expires_at <= ? AND status IN (${CLAIMED_ACTIVE_STATUSES.map(() => "?").join(",")})`)
236
+ .all(now, ...CLAIMED_ACTIVE_STATUSES) as CommandRow[];
184
237
  for (const row of rows) updateCommand(row.id, { status: "timed_out", error: "command timed out" });
185
238
  return rows.map((row) => getCommand(row.id)).filter((command): command is Command => Boolean(command));
186
239
  }
187
240
 
188
241
  function defaultExpiresAt(type: string, now: number): number | undefined {
189
- if (type === "agent.spawn") return now + SPAWN_TTL_MS;
190
- if (type === "agent.shutdown" || type === "agent.restart" || type === "agent.reconnect" || type === "agent.kill" || type === "agent.compact" || type === "agent.clearContext" || type === "agent.injectContext") return now + 30_000;
191
- return undefined;
242
+ const ttlMs = commandTtlMs(type);
243
+ return ttlMs === undefined ? undefined : now + ttlMs;
192
244
  }
193
245
 
194
246
  function recordShutdownTerminalEventOnSuccess(previousStatus: CommandStatus, command: Command): void {
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
+ }
@@ -16,6 +16,7 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
16
16
  import { matchAgents } from "../agent-ref";
17
17
  import { getDb } from "./connection.ts";
18
18
  import { resolveProjectsForCwds } from "./projects.ts";
19
+ import { workspaceActiveClaim } from "../workspace-claim";
19
20
  import type {
20
21
  AgentCard,
21
22
  ActivityEvent,
@@ -96,6 +97,7 @@ function rowToWorkspace(row: any): WorkspaceRecord {
96
97
  mode: row.mode,
97
98
  requestedMode: row.requested_mode ?? undefined,
98
99
  status: row.status,
100
+ orchestratorId: row.orchestrator_id ?? undefined,
99
101
  ownerAgentId: row.owner_agent_id ?? undefined,
100
102
  ownerPolicyName: row.owner_policy_name ?? undefined,
101
103
  ownerAutomationRunId: row.owner_automation_run_id ?? undefined,
@@ -139,7 +141,7 @@ function spawnCommandLandMetadata(spawnRequestId: string | undefined): ReturnTyp
139
141
  return workspaceLandMetadataFrom(parseJson<Record<string, unknown>>(row?.params, {}));
140
142
  }
141
143
 
142
- export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord | null {
144
+ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent, orchestratorId?: string): WorkspaceRecord | null {
143
145
  const workspace = agent.workspace;
144
146
  if (!workspace) return null;
145
147
  if (workspace.mode === "isolated" && workspace.id && workspace.repoRoot && workspace.worktreePath) {
@@ -173,6 +175,7 @@ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceR
173
175
  mode: workspace.mode,
174
176
  requestedMode: workspace.requestedMode,
175
177
  status: preserveStatus ? existing!.status : (workspace.status ?? "active"),
178
+ orchestratorId: existing?.orchestratorId ?? orchestratorId,
176
179
  ownerAgentId: agent.agentId || undefined,
177
180
  ownerPolicyName: agent.policyName,
178
181
  ownerAutomationRunId: agent.automationRunId,
@@ -214,8 +217,8 @@ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceR
214
217
  export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "updatedAt"> & Partial<Pick<WorkspaceRecord, "createdAt" | "updatedAt">>): WorkspaceRecord {
215
218
  const now = Date.now();
216
219
  getDb().query(`
217
- 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)
218
- 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)
219
222
  ON CONFLICT(id) DO UPDATE SET
220
223
  repo_root = excluded.repo_root,
221
224
  source_cwd = excluded.source_cwd,
@@ -226,6 +229,7 @@ export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "upda
226
229
  mode = excluded.mode,
227
230
  requested_mode = excluded.requested_mode,
228
231
  status = excluded.status,
232
+ orchestrator_id = coalesce(excluded.orchestrator_id, workspaces.orchestrator_id),
229
233
  owner_agent_id = coalesce(excluded.owner_agent_id, workspaces.owner_agent_id),
230
234
  owner_policy_name = coalesce(excluded.owner_policy_name, workspaces.owner_policy_name),
231
235
  owner_automation_run_id = coalesce(excluded.owner_automation_run_id, workspaces.owner_automation_run_id),
@@ -245,6 +249,7 @@ export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "upda
245
249
  $mode: input.mode,
246
250
  $requestedMode: input.requestedMode ?? null,
247
251
  $status: input.status,
252
+ $orchestratorId: input.orchestratorId ?? null,
248
253
  $ownerAgentId: input.ownerAgentId ?? null,
249
254
  $ownerPolicyName: input.ownerPolicyName ?? null,
250
255
  $ownerAutomationRunId: input.ownerAutomationRunId ?? null,
@@ -511,6 +516,46 @@ export function patchWorkspaceMetadata(id: string, patch: Record<string, unknown
511
516
  return getWorkspace(id);
512
517
  }
513
518
 
519
+ type WorkspaceClaimResult =
520
+ | { ok: true; workspace: WorkspaceRecord }
521
+ | { ok: false; reason: "not-found" | "claimed-by-other"; workspace?: WorkspaceRecord };
522
+
523
+ export function claimWorkspaceMetadata(
524
+ id: string,
525
+ claimant: string,
526
+ patch: Record<string, unknown>,
527
+ now: number = Date.now(),
528
+ ): WorkspaceClaimResult {
529
+ const run = getDb().transaction((): WorkspaceClaimResult => {
530
+ const existing = getWorkspace(id);
531
+ if (!existing) return { ok: false, reason: "not-found" as const };
532
+ const active = workspaceActiveClaim(existing, now);
533
+ if (active?.by && active.by !== claimant) return { ok: false, reason: "claimed-by-other" as const, workspace: existing };
534
+ const next = { ...existing.metadata, ...patch };
535
+ getDb().query("UPDATE workspaces SET metadata = ? WHERE id = ?").run(JSON.stringify(next), id);
536
+ return { ok: true as const, workspace: getWorkspace(id)! };
537
+ });
538
+ return run();
539
+ }
540
+
541
+ export function releaseWorkspaceClaimMetadata(
542
+ id: string,
543
+ claimant: string,
544
+ patch: Record<string, unknown>,
545
+ now: number = Date.now(),
546
+ ): WorkspaceClaimResult {
547
+ const run = getDb().transaction((): WorkspaceClaimResult => {
548
+ const existing = getWorkspace(id);
549
+ if (!existing) return { ok: false, reason: "not-found" as const };
550
+ const active = workspaceActiveClaim(existing, now);
551
+ if (active?.by && active.by !== claimant) return { ok: false, reason: "claimed-by-other" as const, workspace: existing };
552
+ const next = { ...existing.metadata, ...patch };
553
+ getDb().query("UPDATE workspaces SET metadata = ? WHERE id = ?").run(JSON.stringify(next), id);
554
+ return { ok: true as const, workspace: getWorkspace(id)! };
555
+ });
556
+ return run();
557
+ }
558
+
514
559
  // Re-elect stewards for every repo where an agent owns a live workspace. Called
515
560
  // when an agent (re)registers so a dormant repo regains a steward on rejoin
516
561
  // without a full unscoped sweep.
@@ -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",