agent-relay-server 0.91.4 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/MessageBubble-CUddRP1E.js +3 -0
  4. package/public/assets/{MessageBubble-BORB9AKu.js.map → MessageBubble-CUddRP1E.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BwEOOqdj.js → PlanGraphPanel-CpIBMvnu.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BwEOOqdj.js.map → PlanGraphPanel-CpIBMvnu.js.map} +1 -1
  7. package/public/assets/{activity-DD9bl5uf.js → activity-B6WPWNsX.js} +2 -2
  8. package/public/assets/{activity-DD9bl5uf.js.map → activity-B6WPWNsX.js.map} +1 -1
  9. package/public/assets/{agents-DJgqCIqi.js → agents-BebF-yQr.js} +2 -2
  10. package/public/assets/{agents-DJgqCIqi.js.map → agents-BebF-yQr.js.map} +1 -1
  11. package/public/assets/{automation-Vbei8P9W.js → automation-CyPXC1A8.js} +2 -2
  12. package/public/assets/{automation-Vbei8P9W.js.map → automation-CyPXC1A8.js.map} +1 -1
  13. package/public/assets/{chat-Dw5xpc5W.js → chat-DfqC3kFF.js} +2 -2
  14. package/public/assets/{chat-Dw5xpc5W.js.map → chat-DfqC3kFF.js.map} +1 -1
  15. package/public/assets/display-CWMHll1J.js.map +1 -1
  16. package/public/assets/{formatted-body-impl-BisZ0FtO.js → formatted-body-impl-CrE-hFvy.js} +2 -2
  17. package/public/assets/{formatted-body-impl-BisZ0FtO.js.map → formatted-body-impl-CrE-hFvy.js.map} +1 -1
  18. package/public/assets/{index-Bsi2NH0v.js → index-CdDX5NR1.js} +5 -5
  19. package/public/assets/{index-Bsi2NH0v.js.map → index-CdDX5NR1.js.map} +1 -1
  20. package/public/assets/{insights-D9ytYZyE.js → insights-BribQHXe.js} +2 -2
  21. package/public/assets/{insights-D9ytYZyE.js.map → insights-BribQHXe.js.map} +1 -1
  22. package/public/assets/{maintenance-BA93FvLR.js → maintenance-Cjol3fS_.js} +2 -2
  23. package/public/assets/{maintenance-BA93FvLR.js.map → maintenance-Cjol3fS_.js.map} +1 -1
  24. package/public/assets/{managed-agents-DGwdEIzA.js → managed-agents-1lZwQNHl.js} +2 -2
  25. package/public/assets/{managed-agents-DGwdEIzA.js.map → managed-agents-1lZwQNHl.js.map} +1 -1
  26. package/public/assets/{markdown-preview-impl-C7FDXM5c.js → markdown-preview-impl-C2WeUNFR.js} +2 -2
  27. package/public/assets/{markdown-preview-impl-C7FDXM5c.js.map → markdown-preview-impl-C2WeUNFR.js.map} +1 -1
  28. package/public/assets/{memory-CHzkRJqf.js → memory-B7mcELEW.js} +2 -2
  29. package/public/assets/{memory-CHzkRJqf.js.map → memory-B7mcELEW.js.map} +1 -1
  30. package/public/assets/{messages-B7CJ5tgT.js → messages-B8jF9p2l.js} +2 -2
  31. package/public/assets/{messages-B7CJ5tgT.js.map → messages-B8jF9p2l.js.map} +1 -1
  32. package/public/assets/{orchestrators-DOy9xIx2.js → orchestrators-CQjvBXDx.js} +2 -2
  33. package/public/assets/{orchestrators-DOy9xIx2.js.map → orchestrators-CQjvBXDx.js.map} +1 -1
  34. package/public/assets/{overview-DLbv1BWa.js → overview-CgDUJOG-.js} +2 -2
  35. package/public/assets/{overview-DLbv1BWa.js.map → overview-CgDUJOG-.js.map} +1 -1
  36. package/public/assets/{pairs-BDQo-k3F.js → pairs-BdPKQyzE.js} +2 -2
  37. package/public/assets/{pairs-BDQo-k3F.js.map → pairs-BdPKQyzE.js.map} +1 -1
  38. package/public/assets/{security-9WrCcrWs.js → security-BlZQ0ECI.js} +2 -2
  39. package/public/assets/{security-9WrCcrWs.js.map → security-BlZQ0ECI.js.map} +1 -1
  40. package/public/assets/{select-CRjeGZ9a.js → select-CjpI0JtN.js} +2 -2
  41. package/public/assets/{select-CRjeGZ9a.js.map → select-CjpI0JtN.js.map} +1 -1
  42. package/public/assets/{settings-Durch6xW.js → settings-CMgqpUY-.js} +2 -2
  43. package/public/assets/{settings-Durch6xW.js.map → settings-CMgqpUY-.js.map} +1 -1
  44. package/public/assets/{tasks-86St2vpn.js → tasks-BcHWptn4.js} +2 -2
  45. package/public/assets/{tasks-86St2vpn.js.map → tasks-BcHWptn4.js.map} +1 -1
  46. package/public/assets/{teams-C9WUxzud.js → teams-BGL-VpqZ.js} +2 -2
  47. package/public/assets/{teams-C9WUxzud.js.map → teams-BGL-VpqZ.js.map} +1 -1
  48. package/public/assets/{terminal-viewer-impl-yXWyt2fm.js → terminal-viewer-impl-D8bnx7Z7.js} +2 -2
  49. package/public/assets/{terminal-viewer-impl-yXWyt2fm.js.map → terminal-viewer-impl-D8bnx7Z7.js.map} +1 -1
  50. package/public/assets/{user-avatar-tomKla2H.js → user-avatar-DZ1g4X7l.js} +2 -2
  51. package/public/assets/{user-avatar-tomKla2H.js.map → user-avatar-DZ1g4X7l.js.map} +1 -1
  52. package/public/assets/{work-queue-BtLMzf0g.js → work-queue-CIy6fe6y.js} +2 -2
  53. package/public/assets/{work-queue-BtLMzf0g.js.map → work-queue-CIy6fe6y.js.map} +1 -1
  54. package/public/index.html +1 -1
  55. package/src/bus.ts +9 -2
  56. package/src/config.ts +1 -0
  57. package/src/db/agent-predicates.ts +21 -0
  58. package/src/db/agents.ts +3 -3
  59. package/src/db/delivery.ts +38 -0
  60. package/src/db/messages.ts +18 -1
  61. package/src/db/workspaces.ts +44 -2
  62. package/src/maintenance/constants.ts +2 -0
  63. package/src/maintenance/jobs.ts +21 -8
  64. package/src/maintenance/workspaces.ts +76 -0
  65. package/src/mcp/tools-spawn.ts +30 -2
  66. package/src/routes/agents-spawn.ts +15 -2
  67. package/src/routes/agents.ts +17 -2
  68. package/src/routes/commands.ts +5 -0
  69. package/src/services/managed-running.ts +18 -1
  70. package/src/services/rate-limit-resume.ts +3 -0
  71. package/src/services/register-agent.ts +7 -1
  72. package/src/services/spawn-agent.ts +62 -2
  73. package/src/spawn-command.ts +10 -0
  74. package/src/validation.ts +7 -2
  75. package/src/workspace-auto-merge.ts +53 -9
  76. package/public/assets/MessageBubble-BORB9AKu.js +0 -3
@@ -1,2 +1,2 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{w as s}from"./index-Bsi2NH0v.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
- //# sourceMappingURL=user-avatar-tomKla2H.js.map
1
+ import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{w as s}from"./index-CdDX5NR1.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
+ //# sourceMappingURL=user-avatar-DZ1g4X7l.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar-tomKla2H.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
1
+ {"version":3,"file":"user-avatar-DZ1g4X7l.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
@@ -1,2 +1,2 @@
1
- import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DsqUEZOf.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{V as f,q as p,s as m}from"./index-Bsi2NH0v.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:i,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-BtLMzf0g.js.map
1
+ import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DsqUEZOf.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{V as f,q as p,s as m}from"./index-CdDX5NR1.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:i,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-CIy6fe6y.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-BtLMzf0g.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"work-queue-CIy6fe6y.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"sXAWM,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,SAAS,EAAK,KAAO,YAAY,EAAK,MACpD,+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-Bsi2NH0v.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-CdDX5NR1.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-CP_edrrL.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-CsPXvXMC.js">
package/src/bus.ts CHANGED
@@ -5,7 +5,6 @@ import { projectAgentStatusData } from "./agent-card-projection";
5
5
  import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
6
6
  import { getCommand, updateCommand } from "./commands-db";
7
7
  import { emitCommandEvent } from "./command-events";
8
- import { getLifecycleManager } from "./lifecycle-manager";
9
8
  import { noteAgentTimelineEvent, noteCompactionCommandCompleted } from "./compaction-watch";
10
9
  import { reconcileTerminalProviderExit } from "./services/terminal-provider-exit";
11
10
  import { registerAgent } from "./services/register-agent";
@@ -14,6 +13,8 @@ import { commandAuthorizationResource, dispatchCommand } from "./services/dispat
14
13
  import { auditProviderStateTransition } from "./services/provider-state-audit";
15
14
  import { ServiceAuthError } from "./services/errors";
16
15
  import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
16
+ import { flushQueuedDirectMessages } from "./services/managed-running";
17
+ import { isAgentUnavailable } from "./db/agent-predicates";
17
18
  import { applyCommandToRecipe } from "./recipe-runner";
18
19
  import { enqueueContinuationInjectionAfterSelfResume } from "./self-resume";
19
20
  import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
@@ -121,7 +122,6 @@ export function expireStaleBusAgents(graceMs = busStaleGraceMs()): { agentIds: s
121
122
  revokeRuntimeTokensForAgent(row.id, Date.now(), "stale");
122
123
  setStatus(row.id, "offline");
123
124
  emitAgentStatusEvent(row.id);
124
- getLifecycleManager().onAgentDisappeared(row.id);
125
125
  const tasks = orphanTasksForAgent(row.id);
126
126
  for (const task of tasks) {
127
127
  orphanedTasks.push(task);
@@ -152,10 +152,16 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
152
152
  const current = getAgent(conn.agentId);
153
153
  const heldOffline = current?.status === "offline" && isRecord(current.meta?.terminalProviderExit);
154
154
  if (!heldOffline) {
155
+ const wasUnavailable = current ? isAgentUnavailable(current) : false;
155
156
  const runtime = runtimeFromMeta(frame.payload.meta);
156
157
  heartbeat(conn.agentId, { instanceId: conn.instanceId, epoch: conn.epoch }, runtime);
157
158
  if (frame.payload.status) setStatus(conn.agentId, frame.payload.status, { instanceId: conn.instanceId, epoch: conn.epoch });
158
159
  emitAgentStatusEvent(conn.agentId);
160
+ // #416 — flush queued messages if the agent transitioned from unavailable to available.
161
+ if (wasUnavailable) {
162
+ const after = getAgent(conn.agentId);
163
+ if (after && !isAgentUnavailable(after)) flushQueuedDirectMessages(conn.agentId);
164
+ }
159
165
  }
160
166
  }
161
167
  if (frame.id) send(ws, { type: "ack", payload: { frameId: frame.id } });
@@ -652,6 +658,7 @@ function isContextLifecycleState(value: unknown): value is ContextState["lifecyc
652
658
  value === "working" ||
653
659
  value === "cooling" ||
654
660
  value === "compacting" ||
661
+ value === "clearing-context" ||
655
662
  value === "hibernating";
656
663
  }
657
664
 
package/src/config.ts CHANGED
@@ -154,6 +154,7 @@ export const WORKSPACE_REVIEW_TTL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE
154
154
  export const WORKSPACE_GC_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_GC_INTERVAL_MS", 60 * 60 * 1000);
155
155
  export const WORKSPACE_ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_ORPHAN_REAPER_INTERVAL_MS", 30 * 60 * 1000);
156
156
  export const WORKSPACE_AUTO_MERGE_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_AUTO_MERGE_INTERVAL_MS", CONFLICT_SCAN_INTERVAL_MS);
157
+ export const WORKSPACE_IDLE_REFRESH_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_IDLE_REFRESH_INTERVAL_MS", CONFLICT_SCAN_INTERVAL_MS);
157
158
  export const ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_REAPER_INTERVAL_MS", 5 * 60 * 1000);
158
159
  export const STEWARD_WAKE_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_STEWARD_WAKE_COOLDOWN_MS", 10 * 60 * 1000);
159
160
  export const UNLANDED_FLAG_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_FLAG_COOLDOWN_MS", 6 * 60 * 60 * 1000);
@@ -1,3 +1,4 @@
1
+ import { isProviderBlocked } from "../provider-state.ts";
1
2
  import type { AgentCard } from "../types";
2
3
 
3
4
  export function isDeliveryAgent(agent: AgentCard): boolean {
@@ -7,3 +8,23 @@ export function isDeliveryAgent(agent: AgentCard): boolean {
7
8
  agent.kind !== "channel" &&
8
9
  agent.meta?.kind !== "channel";
9
10
  }
11
+
12
+ /**
13
+ * True when the agent is registered and known to the relay but is currently in a state
14
+ * where it cannot immediately process new messages: stale (missed heartbeats), provider-
15
+ * blocked (rate-limited, API error hold), compacting/clearing context, or registered but
16
+ * not yet ready (starting up).
17
+ *
18
+ * Messages sent to an unavailable agent are stored as `delivery_status = 'queued'` and
19
+ * flushed when the agent transitions back to available (heartbeat, re-registration, or
20
+ * rate-limit-resume). This is distinct from `status === "offline"` which uses the existing
21
+ * store-ahead path for unknown/deleted targets.
22
+ */
23
+ export function isAgentUnavailable(agent: AgentCard): boolean {
24
+ if (agent.status === "stale") return true;
25
+ if (isProviderBlocked(agent)) return true;
26
+ const lcs = agent.context?.lifecycleState;
27
+ if (lcs === "compacting" || lcs === "clearing-context") return true;
28
+ if (!agent.ready && agent.status !== "offline") return true;
29
+ return false;
30
+ }
package/src/db/agents.ts CHANGED
@@ -395,7 +395,7 @@ export function heartbeat(
395
395
  .query(`
396
396
  UPDATE agents SET
397
397
  last_seen = ?,
398
- status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END,
398
+ status = CASE WHEN status IN ('offline', 'stale') THEN 'idle' ELSE status END,
399
399
  provider_capabilities = coalesce(?, provider_capabilities),
400
400
  context_state = coalesce(?, context_state),
401
401
  quota_state = coalesce(?, quota_state)
@@ -412,7 +412,7 @@ export function heartbeat(
412
412
  return result.changes > 0;
413
413
  }
414
414
  const result = getDb()
415
- .query("UPDATE agents SET last_seen = ?, status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END WHERE id = ?")
415
+ .query("UPDATE agents SET last_seen = ?, status = CASE WHEN status IN ('offline', 'stale') THEN 'idle' ELSE status END WHERE id = ?")
416
416
  .run(now, id);
417
417
  return result.changes > 0;
418
418
  }
@@ -495,7 +495,7 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
495
495
  getDb().query("UPDATE agents SET last_seen = ? WHERE id IN ('user', 'system')").run(now);
496
496
  const rows = getDb()
497
497
  .query(
498
- "UPDATE agents SET status = 'offline', ready = 0 WHERE status NOT IN ('offline', 'stale') AND last_seen < ? AND id NOT IN ('user', 'system') RETURNING id"
498
+ "UPDATE agents SET status = 'stale', ready = 0 WHERE status NOT IN ('offline', 'stale') AND last_seen < ? AND id NOT IN ('user', 'system') RETURNING id"
499
499
  )
500
500
  .all(cutoff) as any[];
501
501
  for (const row of rows) {
@@ -413,6 +413,44 @@ export function resolveQueuedPolicyMessages(policyName: string, agentId: string)
413
413
  return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
414
414
  }
415
415
 
416
+ /**
417
+ * Flush queued messages addressed directly to `agentId` (not via a policy target): transition
418
+ * their `delivery_status` from `queued` to `pending` and stamp `resolved_to_agent` so the agent
419
+ * picks them up on its next poll. Ordered by `queued_at ASC, id ASC` (FIFO). Parallel to
420
+ * `resolveQueuedPolicyMessages`, which handles `policy:xxx` targets.
421
+ *
422
+ * Returns the flushed messages so callers can emit `message.available` / `message.new` /
423
+ * `message.delivery_updated` events.
424
+ */
425
+ export function resolveQueuedDirectMessages(agentId: string): Message[] {
426
+ const rows = getDb().query(`
427
+ SELECT m.id
428
+ FROM messages m
429
+ WHERE m.to_target = ?
430
+ AND m.delivery_status = 'queued'
431
+ ORDER BY COALESCE(m.queued_at, m.created_at) ASC, m.id ASC
432
+ `).all(agentId) as Array<{ id: number }>;
433
+ if (rows.length === 0) return [];
434
+ const ids = rows.map((row) => row.id);
435
+ const placeholders = ids.map(() => "?").join(",");
436
+ const now = Date.now();
437
+ getDb().query(`
438
+ UPDATE messages
439
+ SET delivery_status = 'pending',
440
+ resolved_to_agent = ?,
441
+ delivery_attempts = delivery_attempts + 1,
442
+ delivery_last_error = NULL,
443
+ delivery_next_retry_at = NULL,
444
+ delivery_poison_reason = NULL,
445
+ delivery_updated_at = ?
446
+ WHERE id IN (${placeholders})
447
+ `).run(agentId, now, ...ids);
448
+ for (const id of ids) {
449
+ insertMessageDeliveryAttempt(id, { agentId, status: "pending" }, now);
450
+ }
451
+ return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
452
+ }
453
+
416
454
  export function expireQueuedMessages(now: number = Date.now()): Message[] {
417
455
  const rows = getDb().query(`
418
456
  SELECT id FROM messages
@@ -19,6 +19,7 @@ import { getAgent, validateAgentSession } from "./agents.ts";
19
19
  import { cleanAttachmentRefs, linkAttachmentRefs, validateAttachmentRefs } from "./artifacts.ts";
20
20
  import { ClaimError, ValidationError, getDb } from "./connection.ts";
21
21
  import { enforceQueueLimit, isChannelAgentId, matchingDeliveryAgents } from "./delivery.ts";
22
+ import { isAgentUnavailable } from "./agent-predicates.ts";
22
23
  import { MSG_SELECT, normalizeReactionEmoji, rowToMessage } from "./mappers.ts";
23
24
  import { claimMessageRow } from "./message-claims.ts";
24
25
  import { getMessage } from "./message-queries.ts";
@@ -469,6 +470,19 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
469
470
  queuedAt = now;
470
471
  maxAgeSeconds = maxAgeSeconds ?? 86_400;
471
472
  }
473
+ } else if (!isFanoutTarget(input.to) && !isMechanicalMessageKind(input.kind)) {
474
+ // #416 — queue messages to a registered-but-temporarily-unavailable direct target so they
475
+ // are not silently held as "pending" while the agent cannot process them. The flush fires
476
+ // when the agent returns to available (heartbeat stale→idle, re-registration, rate-limit
477
+ // resume). Offline agents use the existing store-ahead path (policyName is null, status
478
+ // "offline" → resolveQueuedDirectMessages is never called for them — the queue is cleared
479
+ // only when the agent actually comes back as non-offline).
480
+ const directAgent = getAgent(input.to);
481
+ if (directAgent && isAgentUnavailable(directAgent)) {
482
+ deliveryStatus = "queued";
483
+ queuedAt = now;
484
+ maxAgeSeconds = maxAgeSeconds ?? 86_400;
485
+ }
472
486
  }
473
487
 
474
488
  // #451/#461 — moderate default TTL for non-claimable FAN-OUT messages the sender didn't bound,
@@ -509,7 +523,10 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
509
523
  const newId = Number(result.lastInsertRowid);
510
524
  if (threadId === null) setSelfThread.run(newId, newId);
511
525
  linkAttachmentRefs("message", newId, attachmentRefs, input.from);
512
- if (policyName && deliveryStatus === "queued") enforceQueueLimit(`policy:${policyName}`);
526
+ if (deliveryStatus === "queued") {
527
+ if (policyName) enforceQueueLimit(`policy:${policyName}`);
528
+ else enforceQueueLimit(input.to);
529
+ }
513
530
  return newId;
514
531
  })();
515
532
 
@@ -74,10 +74,15 @@ import type {
74
74
  ProviderCapabilities,
75
75
  TaskStatusInput,
76
76
  WorkspaceMetadata,
77
+ WorkspaceAutoMergePolicy,
78
+ WorkspaceLandingPolicy,
77
79
  WorkspaceRecord,
78
80
  WorkspaceStatus,
79
81
  } from "../types";
80
82
 
83
+ const SPAWN_LAND_STRATEGIES = ["direct", "pr"] as const;
84
+ const SPAWN_AUTO_MERGE_POLICIES = ["on-green", "on-approval", "manual"] as const;
85
+
81
86
  function rowToWorkspace(row: any): WorkspaceRecord {
82
87
  return {
83
88
  id: row.id,
@@ -102,6 +107,36 @@ function rowToWorkspace(row: any): WorkspaceRecord {
102
107
  };
103
108
  }
104
109
 
110
+ function workspaceLandMetadataFrom(value: Record<string, unknown> | undefined): {
111
+ landStrategy?: WorkspaceLandingPolicy;
112
+ autoMerge?: WorkspaceAutoMergePolicy;
113
+ reviewer?: string;
114
+ } {
115
+ if (!value) return {};
116
+ const landStrategy = typeof value.landStrategy === "string" && (SPAWN_LAND_STRATEGIES as readonly string[]).includes(value.landStrategy)
117
+ ? value.landStrategy as WorkspaceLandingPolicy
118
+ : undefined;
119
+ const autoMerge = typeof value.autoMerge === "string" && (SPAWN_AUTO_MERGE_POLICIES as readonly string[]).includes(value.autoMerge)
120
+ ? value.autoMerge as WorkspaceAutoMergePolicy
121
+ : undefined;
122
+ const reviewer = typeof value.reviewer === "string" && value.reviewer.trim() ? value.reviewer.trim() : undefined;
123
+ return {
124
+ ...(landStrategy ? { landStrategy } : {}),
125
+ ...(autoMerge ? { autoMerge } : {}),
126
+ ...(reviewer ? { reviewer } : {}),
127
+ };
128
+ }
129
+
130
+ function spawnCommandLandMetadata(spawnRequestId: string | undefined): ReturnType<typeof workspaceLandMetadataFrom> {
131
+ if (!spawnRequestId) return {};
132
+ const row = getDb().query(`
133
+ SELECT params FROM commands
134
+ WHERE type = 'agent.spawn' AND correlation_id = ?
135
+ ORDER BY created_at DESC
136
+ LIMIT 1
137
+ `).get(spawnRequestId) as { params?: string } | undefined;
138
+ return workspaceLandMetadataFrom(parseJson<Record<string, unknown>>(row?.params, {}));
139
+ }
105
140
 
106
141
  export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord | null {
107
142
  const workspace = agent.workspace;
@@ -140,7 +175,15 @@ export function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceR
140
175
  ownerAgentId: agent.agentId || undefined,
141
176
  ownerPolicyName: agent.policyName,
142
177
  ownerAutomationRunId: agent.automationRunId,
143
- metadata: { ...(existing?.metadata ?? {}), provider: agent.provider, label: agent.label, sessionName: agent.sessionName, tmuxSession: agent.tmuxSession },
178
+ metadata: {
179
+ ...(existing?.metadata ?? {}),
180
+ ...spawnCommandLandMetadata(agent.spawnRequestId),
181
+ ...workspaceLandMetadataFrom(workspace as unknown as Record<string, unknown>),
182
+ provider: agent.provider,
183
+ label: agent.label,
184
+ sessionName: agent.sessionName,
185
+ tmuxSession: agent.tmuxSession,
186
+ },
144
187
  });
145
188
  }
146
189
  // Shared-mode occupancy: one row per agent sharing a git repo, with no
@@ -438,4 +481,3 @@ export function electWorkspaceStewardsForAgent(agentId: string): void {
438
481
  `).all(agentId) as Array<{ repo_root: string }>;
439
482
  for (const r of repos) electWorkspaceStewards(r.repo_root);
440
483
  }
441
-
@@ -11,6 +11,7 @@ import {
11
11
  TOKEN_RECORD_RETENTION_SECONDS,
12
12
  WORKSPACE_AUTO_MERGE_INTERVAL_MS,
13
13
  WORKSPACE_GC_INTERVAL_MS,
14
+ WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
14
15
  WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
15
16
  WORKSPACE_RETENTION_MS,
16
17
  WORKSPACE_REVIEW_TTL_MS,
@@ -45,6 +46,7 @@ export {
45
46
  TOKEN_RECORD_RETENTION_SECONDS,
46
47
  WORKSPACE_AUTO_MERGE_INTERVAL_MS,
47
48
  WORKSPACE_GC_INTERVAL_MS,
49
+ WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
48
50
  WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
49
51
  WORKSPACE_RETENTION_MS,
50
52
  WORKSPACE_REVIEW_TTL_MS,
@@ -53,6 +53,7 @@ import {
53
53
  QUOTA_HISTORY_RETENTION_MS,
54
54
  TOKEN_RECORD_RETENTION_SECONDS,
55
55
  WORKSPACE_AUTO_MERGE_INTERVAL_MS,
56
+ WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
56
57
  WORKSPACE_GC_INTERVAL_MS,
57
58
  WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
58
59
  CONFLICT_SCAN_INTERVAL_MS,
@@ -61,7 +62,7 @@ import {
61
62
  import { emitCommand } from "./events";
62
63
  import { reapOrphanedSessions } from "./orphaned-sessions";
63
64
  import { reapZeroMemberTeams, runTeamIdleWatchdog } from "./teams";
64
- import { scanWorkspaceConflicts, workspaceGC } from "./workspaces";
65
+ import { refreshIdleWorkspaces, scanWorkspaceConflicts, workspaceGC } from "./workspaces";
65
66
  import type { MaintenanceJobDefinition } from "./types";
66
67
 
67
68
  let dbMaintenanceRuns = 0;
@@ -227,6 +228,14 @@ export const definitions: MaintenanceJobDefinition[] = [
227
228
  runOnStart: true,
228
229
  handler() {
229
230
  const expired = expireStaleBusAgents();
231
+ for (const id of expired.agentIds) {
232
+ getLifecycleManager().onAgentDisappeared(id);
233
+ // #636/#659 — only after the stale reconnect grace expires do we turn a bare
234
+ // heartbeat gap into a terminal parent notification. A live runner that
235
+ // reconnects before this point heals from `stale` without a false exit.
236
+ const diagnosis = diagnoseAgentDeath({ agent: getAgent(id), reason: "heartbeat lost" });
237
+ notifyAgentOffline(id, "heartbeat lost", diagnosis);
238
+ }
230
239
  for (const task of expired.orphanedTasks) emitTaskChanged(task, "task.updated");
231
240
  return {
232
241
  expiredBusAgentIds: expired.agentIds,
@@ -244,16 +253,11 @@ export const definitions: MaintenanceJobDefinition[] = [
244
253
  const reapedAgentIds = reapStaleAgents(STALE_TTL_MS);
245
254
  for (const id of reapedAgentIds) {
246
255
  emitAgentStatus(id);
247
- getLifecycleManager().onAgentDisappeared(id);
248
- // #636 — a heartbeat-lost death still gets a best-effort post-mortem from the agent's last
249
- // known meta (lastExit/lastError/auth) + heartbeat gap, riding the terminal event.
250
- const diagnosis = diagnoseAgentDeath({ agent: getAgent(id), reason: "heartbeat lost" });
251
- notifyAgentOffline(id, "heartbeat lost", diagnosis);
252
256
  createActivityEvent({
253
257
  clientId: "server-agent-" + id + "-heartbeat-lost-" + Date.now(),
254
258
  kind: "state",
255
- title: "Heartbeat lost",
256
- body: "Agent marked offline",
259
+ title: "Heartbeat stale",
260
+ body: "Agent entered reconnect grace",
257
261
  meta: id,
258
262
  icon: "ti-heart-off",
259
263
  view: "chat",
@@ -442,6 +446,15 @@ export const definitions: MaintenanceJobDefinition[] = [
442
446
  timeoutMs: 60 * 1000,
443
447
  handler: autoMergeCleanFastForwards,
444
448
  },
449
+ {
450
+ id: "workspace-idle-refresh",
451
+ title: "Workspace idle refresh",
452
+ description: "Proactively advance idle branch-agent workspaces to origin/main (#478): for each active, clean (dirty=0, ahead=0), owner-idle workspace, dispatch a workspace.idle-refresh command so the agent wakes already current and needs no ready/status dance.",
453
+ intervalMs: WORKSPACE_IDLE_REFRESH_INTERVAL_MS,
454
+ runOnStart: false,
455
+ timeoutMs: 60 * 1000,
456
+ handler: refreshIdleWorkspaces,
457
+ },
445
458
  {
446
459
  id: "workspace-gc",
447
460
  title: "Workspace GC",
@@ -11,6 +11,7 @@ import {
11
11
  reelectRepoSteward,
12
12
  releaseExpiredMergeLeases,
13
13
  updateWorkspaceStatus,
14
+ setWorkspaceBranch,
14
15
  } from "../db";
15
16
  import type { WorkspaceMergePreview, WorkspaceStatus } from "../types";
16
17
  import { notifyStewardHandoff, reconcileLandedWorkspace } from "../branch-landed";
@@ -262,3 +263,78 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
262
263
  escalationTargets,
263
264
  };
264
265
  }
266
+
267
+ /**
268
+ * Proactively refresh idle branch-agent workspaces against origin/main (#478).
269
+ *
270
+ * For each `active` isolated workspace whose owner agent is NOT busy and whose
271
+ * stored probe data indicates no WIP (dirty=0, ahead=0), dispatch a
272
+ * `workspace.idle-refresh` command to the owning orchestrator. The orchestrator
273
+ * re-verifies liveness, fetches origin, and — when the worktree is cleanly
274
+ * behind — performs a `checkout -B <fresh> origin/main` so the agent is already
275
+ * current when it next wakes. Workspaces with WIP, commits ahead, or busy owners
276
+ * are skipped silently.
277
+ */
278
+ export async function refreshIdleWorkspaces(): Promise<Record<string, unknown>> {
279
+ const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.agentId);
280
+ if (!orchestrators.length) return { dispatched: 0, skipped: 0, reason: "no online orchestrators" };
281
+
282
+ const candidates = listWorkspaces().filter(
283
+ (ws) => ws.mode === "isolated" && ws.status === "active" && Boolean(ws.worktreePath),
284
+ );
285
+
286
+ const dispatched: string[] = [];
287
+ const skipped: string[] = [];
288
+ const now = Date.now();
289
+
290
+ for (const ws of candidates) {
291
+ const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
292
+ if (!orch?.agentId) { skipped.push(`${ws.id}:no-orchestrator`); continue; }
293
+
294
+ // Gate: owner must exist and not be busy.
295
+ const owner = ws.ownerAgentId ? getAgent(ws.ownerAgentId) : null;
296
+ if (!owner || owner.status === "busy") { skipped.push(`${ws.id}:owner-busy`); continue; }
297
+
298
+ // Gate: skip if stored metadata shows WIP or commits ahead.
299
+ // The orchestrator re-checks live before mutating; stored data is a fast pre-filter.
300
+ const meta = ws.metadata as Record<string, unknown>;
301
+ const gitAhead = typeof meta.gitAhead === "number" ? meta.gitAhead : undefined;
302
+ const gitDirtyCount = typeof meta.gitDirtyCount === "number" ? meta.gitDirtyCount : undefined;
303
+ if (gitAhead !== undefined && gitAhead > 0) { skipped.push(`${ws.id}:ahead`); continue; }
304
+ if (gitDirtyCount !== undefined && gitDirtyCount > 0) { skipped.push(`${ws.id}:dirty`); continue; }
305
+
306
+ const command = createCommand({
307
+ type: "workspace.idle-refresh",
308
+ source: "system",
309
+ target: orch.agentId,
310
+ correlationId: ws.id,
311
+ params: {
312
+ workspaceId: ws.id,
313
+ repoRoot: ws.repoRoot,
314
+ worktreePath: ws.worktreePath,
315
+ branch: ws.branch,
316
+ baseRef: ws.baseRef,
317
+ baseSha: ws.baseSha,
318
+ requestedBy: "workspace-idle-refresh",
319
+ requestedAt: now,
320
+ },
321
+ });
322
+ emitCommand(command);
323
+ dispatched.push(ws.id);
324
+ }
325
+
326
+ return { dispatched, skipped };
327
+ }
328
+
329
+ /**
330
+ * Apply the result of a settled `workspace.idle-refresh` command.
331
+ * When the orchestrator refreshed the worktree, repoint the DB row to the
332
+ * new branch and update base_sha so the next merge targets the live branch.
333
+ */
334
+ export function applyIdleRefreshResult(workspaceId: string, result: Record<string, unknown>): boolean {
335
+ const newBranch = typeof result.newBranch === "string" ? result.newBranch : undefined;
336
+ const baseSha = typeof result.baseSha === "string" ? result.baseSha : undefined;
337
+ if (!newBranch || result.refreshed !== true) return false;
338
+ setWorkspaceBranch(workspaceId, newBranch, baseSha);
339
+ return true;
340
+ }