agent-relay-server 0.97.0 → 0.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +55 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-CIDyX8f7.js → MessageBubble-C4AeEHdz.js} +2 -2
- package/public/assets/{MessageBubble-CIDyX8f7.js.map → MessageBubble-C4AeEHdz.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BmWB81w_.js → PlanGraphPanel-cwPUGFXA.js} +2 -2
- package/public/assets/{PlanGraphPanel-BmWB81w_.js.map → PlanGraphPanel-cwPUGFXA.js.map} +1 -1
- package/public/assets/{activity-QQikHZe4.js → activity-BWGwnSg5.js} +2 -2
- package/public/assets/{activity-QQikHZe4.js.map → activity-BWGwnSg5.js.map} +1 -1
- package/public/assets/{agent-profiles-WvAN5cR_.js → agent-profiles-C6N9cTJg.js} +2 -2
- package/public/assets/{agent-profiles-WvAN5cR_.js.map → agent-profiles-C6N9cTJg.js.map} +1 -1
- package/public/assets/{agents-BsFfumLn.js → agents-FUBxhCEU.js} +2 -2
- package/public/assets/{agents-BsFfumLn.js.map → agents-FUBxhCEU.js.map} +1 -1
- package/public/assets/{automation-BTbpiINE.js → automation-CMketzfB.js} +2 -2
- package/public/assets/{automation-BTbpiINE.js.map → automation-CMketzfB.js.map} +1 -1
- package/public/assets/{chat-BlFUmD3r.js → chat-BV_pnRhk.js} +2 -2
- package/public/assets/{chat-BlFUmD3r.js.map → chat-BV_pnRhk.js.map} +1 -1
- package/public/assets/{formatted-body-impl-Dbjjdcu9.js → formatted-body-impl-DzHQTBmn.js} +2 -2
- package/public/assets/{formatted-body-impl-Dbjjdcu9.js.map → formatted-body-impl-DzHQTBmn.js.map} +1 -1
- package/public/assets/index-CYLrIuJT.css +2 -0
- package/public/assets/{index-CPXz74W5.js → index-CdEAo41I.js} +7 -7
- package/public/assets/index-CdEAo41I.js.map +1 -0
- package/public/assets/{insights-D1Oq25BL.js → insights-C_pJNcFD.js} +2 -2
- package/public/assets/{insights-D1Oq25BL.js.map → insights-C_pJNcFD.js.map} +1 -1
- package/public/assets/{maintenance-CgXJwFTo.js → maintenance-Ckz5BAor.js} +2 -2
- package/public/assets/{maintenance-CgXJwFTo.js.map → maintenance-Ckz5BAor.js.map} +1 -1
- package/public/assets/{managed-agents-CbuFHwWE.js → managed-agents-BbZclnqP.js} +2 -2
- package/public/assets/{managed-agents-CbuFHwWE.js.map → managed-agents-BbZclnqP.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-B79bSsKK.js → markdown-preview-impl-CtuHw6Mm.js} +2 -2
- package/public/assets/{markdown-preview-impl-B79bSsKK.js.map → markdown-preview-impl-CtuHw6Mm.js.map} +1 -1
- package/public/assets/{memory-Box_rMj_.js → memory-C5DK3rhW.js} +2 -2
- package/public/assets/{memory-Box_rMj_.js.map → memory-C5DK3rhW.js.map} +1 -1
- package/public/assets/{messages-BXIHBaN2.js → messages-CsjtiFEb.js} +2 -2
- package/public/assets/{messages-BXIHBaN2.js.map → messages-CsjtiFEb.js.map} +1 -1
- package/public/assets/{orchestrators-MNj_mp3U.js → orchestrators-D9fMiBRy.js} +2 -2
- package/public/assets/{orchestrators-MNj_mp3U.js.map → orchestrators-D9fMiBRy.js.map} +1 -1
- package/public/assets/{overview-KfwJgCUa.js → overview-BLWoCnNQ.js} +2 -2
- package/public/assets/{overview-KfwJgCUa.js.map → overview-BLWoCnNQ.js.map} +1 -1
- package/public/assets/{pairs-DfrGWlgL.js → pairs-HnvpLz4C.js} +2 -2
- package/public/assets/{pairs-DfrGWlgL.js.map → pairs-HnvpLz4C.js.map} +1 -1
- package/public/assets/{registry-CiBC97sL.js → registry-BqAS3ADV.js} +2 -2
- package/public/assets/{registry-CiBC97sL.js.map → registry-BqAS3ADV.js.map} +1 -1
- package/public/assets/{security-BbU_vIdb.js → security-CWgtcQ2X.js} +2 -2
- package/public/assets/{security-BbU_vIdb.js.map → security-CWgtcQ2X.js.map} +1 -1
- package/public/assets/{select-CeQwmVTm.js → select-UdIYiKFs.js} +2 -2
- package/public/assets/{select-CeQwmVTm.js.map → select-UdIYiKFs.js.map} +1 -1
- package/public/assets/settings-ruDxPuza.js +6 -0
- package/public/assets/settings-ruDxPuza.js.map +1 -0
- package/public/assets/{tasks-za0RxIQw.js → tasks-DomQFwZP.js} +2 -2
- package/public/assets/{tasks-za0RxIQw.js.map → tasks-DomQFwZP.js.map} +1 -1
- package/public/assets/{teams-ByAqwLG9.js → teams-CXNp0U3B.js} +2 -2
- package/public/assets/{teams-ByAqwLG9.js.map → teams-CXNp0U3B.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CkoWD5D_.js → terminal-viewer-impl-DiWBtzvZ.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CkoWD5D_.js.map → terminal-viewer-impl-DiWBtzvZ.js.map} +1 -1
- package/public/assets/{user-avatar-Q_N6iHEn.js → user-avatar-DBs9Koqp.js} +2 -2
- package/public/assets/{user-avatar-Q_N6iHEn.js.map → user-avatar-DBs9Koqp.js.map} +1 -1
- package/public/assets/{work-queue-DfRo1B2P.js → work-queue-BD29QuLd.js} +2 -2
- package/public/assets/{work-queue-DfRo1B2P.js.map → work-queue-BD29QuLd.js.map} +1 -1
- package/public/index.html +2 -2
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapter.ts +9 -1
- package/src/agent-busy-health.ts +62 -63
- package/src/codex-wham-quota.ts +5 -76
- package/src/compaction-watch.ts +7 -6
- package/src/db/provider-quota-burn.ts +6 -18
- package/src/db/provider-quotas.ts +27 -12
- package/src/lifecycle-manager.ts +4 -2
- package/src/maintenance/jobs.ts +1 -1
- package/src/notification-types.ts +3 -1
- package/src/quota.ts +1 -1
- package/src/routes/index.ts +3 -0
- package/src/routes/routing-policy.ts +25 -0
- package/src/routing-policy-push.ts +82 -0
- package/src/routing-policy-store.ts +148 -0
- package/src/routing-rule-resolver.ts +87 -0
- package/src/security.ts +6 -0
- package/src/spawn-targets.ts +44 -12
- package/public/assets/index-CPWbr4sq.css +0 -2
- package/public/assets/index-CPXz74W5.js.map +0 -1
- package/public/assets/settings-DBsL3jb7.js +0 -6
- package/public/assets/settings-DBsL3jb7.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{Qn as t,Xn as n,dr as r,er as i}from"./lucide-react-BYZuAhQs.js";import{_ as a,rt as o}from"./display-DRiGi1zj.js";import{D as s}from"./index-
|
|
2
|
-
//# sourceMappingURL=user-avatar-
|
|
1
|
+
import{r as e}from"./chunk-CilyBKbf.js";import{Qn as t,Xn as n,dr as r,er as i}from"./lucide-react-BYZuAhQs.js";import{_ as a,rt as o}from"./display-DRiGi1zj.js";import{D as s}from"./index-CdEAo41I.js";var c=e(r(),1),l=t(),u={id:o};function d({user:e,address:t,distinct:r=!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 n=await i(d);if(e)return;t=URL.createObjectURL(n),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:n(`w-7 h-7 rounded-md object-cover`,o)});if(r){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:n(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(t),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
|
|
2
|
+
//# sourceMappingURL=user-avatar-DBs9Koqp.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-avatar-
|
|
1
|
+
{"version":3,"file":"user-avatar-DBs9Koqp.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{Dn as e,Qn as t,hn as n,i as r,pt as i}from"./lucide-react-BYZuAhQs.js";import{l as a,t as o}from"./store-pKbf_WYd.js";import{K as s,ct as c,d as l}from"./display-DRiGi1zj.js";import{t as u}from"./badge-BLar6FjP.js";import{t as d}from"./button-BrIB69qd.js";import{W as f,X as p,u as m}from"./index-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
1
|
+
import{Dn as e,Qn as t,hn as n,i as r,pt as i}from"./lucide-react-BYZuAhQs.js";import{l as a,t as o}from"./store-pKbf_WYd.js";import{K as s,ct as c,d as l}from"./display-DRiGi1zj.js";import{t as u}from"./badge-BLar6FjP.js";import{t as d}from"./button-BrIB69qd.js";import{W as f,X as p,u as m}from"./index-CdEAo41I.js";import{r as h,t as g}from"./card-lhnFnBoz.js";var _=t(),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),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=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&&c(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: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:t,now:i,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.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:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.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`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),s(i,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.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:l,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
|
|
2
|
+
//# sourceMappingURL=work-queue-BD29QuLd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-queue-DfRo1B2P.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-BD29QuLd.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
|
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
|
|
13
13
|
<link rel="manifest" href="manifest.webmanifest">
|
|
14
14
|
<link rel="apple-touch-icon" href="icons/agent-relay-192.png">
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-CdEAo41I.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-BYZuAhQs.js">
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
<link rel="modulepreload" crossorigin href="./assets/badge-BLar6FjP.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="./assets/input-B_fUHtLD.js">
|
|
40
40
|
<link rel="modulepreload" crossorigin href="./assets/themes-DjSwmAZN.js">
|
|
41
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
41
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CYLrIuJT.css">
|
|
42
42
|
</head>
|
|
43
43
|
<body class="bg-background text-foreground antialiased">
|
|
44
44
|
<div id="root"></div>
|
package/runner/src/adapter.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AgentProfile, Message } from "agent-relay-sdk";
|
|
1
|
+
import type { AgentProfile, LivenessSignal, Message } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
|
+
import type { LivenessInputs } from "./liveness";
|
|
3
4
|
import type { SessionEvent } from "./session-insights";
|
|
4
5
|
import { messageBodyMaxCharsFromEnv } from "./config";
|
|
5
6
|
import { relayReplyReminderText } from "./relay-instructions";
|
|
@@ -191,6 +192,13 @@ export interface ProviderAdapter {
|
|
|
191
192
|
// (Codex app-server item events). Claude mirrors via hooks/transcript instead,
|
|
192
193
|
// so it leaves this unimplemented.
|
|
193
194
|
onSessionEvent?(cb: (event: ProviderSessionEvent) => void): void;
|
|
195
|
+
// §4.1 of the Runner↔provider contract (#725 / #746): fold the runner-tracked raw
|
|
196
|
+
// transport/timeline/token/work-kind signals into ONE normalized LivenessSignal the
|
|
197
|
+
// server reads instead of six raw `meta` fields. Each adapter owns what counts as
|
|
198
|
+
// progress for its provider (Claude #713 timeline transitions; Codex #724 live
|
|
199
|
+
// subprocess work). Optional so test/mock adapters fall back to the shared
|
|
200
|
+
// {@link computeLivenessSignal} fold — the runner always has a verdict to ship.
|
|
201
|
+
liveness?(inputs: LivenessInputs): LivenessSignal;
|
|
194
202
|
// Headless providers with no tmux session (e.g. the Codex app-server) still
|
|
195
203
|
// warrant an automatic restart on unexpected exit. Returning true opts the
|
|
196
204
|
// provider into the runner's restart-with-backoff path.
|
package/src/agent-busy-health.ts
CHANGED
|
@@ -5,14 +5,13 @@ import {
|
|
|
5
5
|
STUCK_BUSY_STARTUP_MS,
|
|
6
6
|
STUCK_BUSY_TRANSPORT_STALE_MS,
|
|
7
7
|
} from "./config";
|
|
8
|
-
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
8
|
+
import { isRecord, stringValue, type LivenessSignal } from "agent-relay-sdk";
|
|
9
9
|
import type { AgentCard } from "./types";
|
|
10
10
|
|
|
11
11
|
type BusyHealthKind = "provider-turn" | "startup" | "reconnect" | "shutdown";
|
|
12
12
|
|
|
13
13
|
interface ActiveWorkLike {
|
|
14
14
|
id?: unknown;
|
|
15
|
-
kind?: unknown;
|
|
16
15
|
label?: unknown;
|
|
17
16
|
startedAt?: unknown;
|
|
18
17
|
}
|
|
@@ -22,18 +21,19 @@ export interface AgentBusyHealth {
|
|
|
22
21
|
kind: BusyHealthKind;
|
|
23
22
|
thresholdMs: number;
|
|
24
23
|
activeWorkId?: string;
|
|
25
|
-
activeWorkKind?: string;
|
|
26
24
|
activeWorkLabel?: string;
|
|
27
25
|
activeWorkStartedAt: number;
|
|
28
26
|
activeWorkAgeMs: number;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/** Most recent of lastProviderEventAt and any fresh lifecycle/timeline transition (e.g. `compacting`). */
|
|
27
|
+
/** Most recent forward-progress timestamp from the contract LivenessSignal (provider tokens,
|
|
28
|
+
* a lifecycle/timeline transition, or live tool/subprocess work), floored at the work start. */
|
|
32
29
|
lastProgressAt: number;
|
|
33
|
-
/** Most recent transport
|
|
30
|
+
/** Most recent transport-liveness timestamp: LivenessSignal.transportLiveAt folded with the
|
|
31
|
+
* server-stamped agent.lastSeen as the floor. Always set. */
|
|
34
32
|
transportLiveAt: number;
|
|
35
|
-
/** Age of the most recent transport
|
|
33
|
+
/** Age of the most recent transport-liveness signal. */
|
|
36
34
|
transportStaleForMs: number;
|
|
35
|
+
/** True while the contract reports live tool/subprocess work (vetoes stuck-busy). */
|
|
36
|
+
hasLiveWork: boolean;
|
|
37
37
|
staleForMs: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -45,37 +45,35 @@ function activeWork(agent: AgentCard): ActiveWorkLike[] {
|
|
|
45
45
|
return Array.isArray(agent.meta?.activeWork) ? agent.meta.activeWork as ActiveWorkLike[] : [];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const transport = isRecord(agent.meta?.transport) ? agent.meta.transport : undefined;
|
|
65
|
-
const frame = transport ? numberValue(transport.lastServerFrameAt) : undefined;
|
|
66
|
-
const ack = transport ? numberValue(transport.lastHeartbeatAckAt) : undefined;
|
|
67
|
-
// agent.lastSeen is stamped by the server on every heartbeat — it is always a valid transport
|
|
68
|
-
// liveness proxy even when the runner does not populate meta.transport.
|
|
69
|
-
return Math.max(frame ?? 0, ack ?? 0, agent.lastSeen);
|
|
48
|
+
// The work record that drives the busy AGE — the earliest-started one. Read only for the age
|
|
49
|
+
// threshold + display (id/label); the busy KIND comes from lifecycle markers (below) and the
|
|
50
|
+
// live-work question comes from LivenessSignal.hasLiveWork. We deliberately never enumerate
|
|
51
|
+
// `activeWork[].kind` here — that interpretation now lives behind the contract (#746).
|
|
52
|
+
function oldestWork(works: ActiveWorkLike[]): { work?: ActiveWorkLike; startedAt?: number } {
|
|
53
|
+
let best: ActiveWorkLike | undefined;
|
|
54
|
+
let bestAt: number | undefined;
|
|
55
|
+
for (const work of works) {
|
|
56
|
+
const at = numberValue(work.startedAt);
|
|
57
|
+
if (at === undefined) continue;
|
|
58
|
+
if (bestAt === undefined || at < bestAt) {
|
|
59
|
+
best = work;
|
|
60
|
+
bestAt = at;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { work: best ?? works[0], startedAt: bestAt };
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
// The ONLY runtime-liveness input the server reads: the normalized contract verdict the runner
|
|
67
|
+
// (per-provider adapter) ships on the card. Replaces the six raw `meta` reads (#713/#724) the
|
|
68
|
+
// stuck-busy detector used to re-derive. Returns undefined for cards from a pre-contract runner;
|
|
69
|
+
// the caller then degrades gracefully to the server-stamped lastSeen + work-start floors.
|
|
70
|
+
function readLiveness(agent: AgentCard): LivenessSignal | undefined {
|
|
71
|
+
const raw = agent.meta?.liveness;
|
|
72
|
+
if (!isRecord(raw)) return undefined;
|
|
73
|
+
const transportLiveAt = numberValue(raw.transportLiveAt);
|
|
74
|
+
const lastProgressAt = numberValue(raw.lastProgressAt);
|
|
75
|
+
if (transportLiveAt === undefined || lastProgressAt === undefined) return undefined;
|
|
76
|
+
return { transportLiveAt, lastProgressAt, hasLiveWork: raw.hasLiveWork === true };
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
function thresholdFor(kind: BusyHealthKind): number {
|
|
@@ -85,64 +83,65 @@ function thresholdFor(kind: BusyHealthKind): number {
|
|
|
85
83
|
return STUCK_BUSY_PROVIDER_TURN_MS;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
// Busy KIND classification — driven by provider-agnostic lifecycle markers (lifecycleAction /
|
|
87
|
+
// ready), NOT by provider runtime signals. The active-work set contributes only the start
|
|
88
|
+
// timestamp (age) and display id/label.
|
|
89
|
+
function classifyBusy(agent: AgentCard): { kind: BusyHealthKind; work?: ActiveWorkLike; startedAt: number } | null {
|
|
89
90
|
if (agent.status !== "busy") return null;
|
|
90
|
-
const
|
|
91
|
-
const providerTurn = works.find((item) => item.kind === "provider-turn");
|
|
92
|
-
if (providerTurn) {
|
|
93
|
-
return { kind: "provider-turn", work: providerTurn, startedAt: numberValue(providerTurn.startedAt) ?? agent.lastSeen };
|
|
94
|
-
}
|
|
95
|
-
|
|
91
|
+
const oldest = oldestWork(activeWork(agent));
|
|
96
92
|
const lifecycleAction = stringValue(agent.meta?.lifecycleAction) ?? "";
|
|
97
93
|
const lifecycleActionAt = numberValue(agent.meta?.lifecycleActionAt);
|
|
98
94
|
if (lifecycleAction.includes("shutdown") || lifecycleAction === "killing") {
|
|
99
|
-
return { kind: "shutdown", work:
|
|
95
|
+
return { kind: "shutdown", work: oldest.work, startedAt: lifecycleActionAt ?? oldest.startedAt ?? agent.lastSeen };
|
|
100
96
|
}
|
|
101
97
|
if (lifecycleAction.includes("reconnect") || lifecycleAction === "resuming") {
|
|
102
|
-
return { kind: "reconnect", work:
|
|
98
|
+
return { kind: "reconnect", work: oldest.work, startedAt: lifecycleActionAt ?? oldest.startedAt ?? agent.lastSeen };
|
|
103
99
|
}
|
|
104
100
|
if (!agent.ready) {
|
|
105
|
-
return { kind: "startup", work:
|
|
101
|
+
return { kind: "startup", work: oldest.work, startedAt: numberValue(agent.meta?.startedAt) ?? agent.createdAt };
|
|
106
102
|
}
|
|
107
|
-
return
|
|
103
|
+
return oldest.work ? { kind: "provider-turn", work: oldest.work, startedAt: oldest.startedAt ?? agent.lastSeen } : null;
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
export function deriveAgentBusyHealth(agent: AgentCard, now: number = Date.now()): AgentBusyHealth | undefined {
|
|
111
|
-
const classified = classifyBusy(agent
|
|
107
|
+
const classified = classifyBusy(agent);
|
|
112
108
|
if (!classified) return undefined;
|
|
113
|
-
const works = activeWork(agent);
|
|
114
109
|
const thresholdMs = thresholdFor(classified.kind);
|
|
115
110
|
const activeWorkStartedAt = Math.max(0, classified.startedAt);
|
|
116
111
|
const activeWorkAgeMs = Math.max(0, now - activeWorkStartedAt);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
112
|
+
|
|
113
|
+
const liveness = readLiveness(agent);
|
|
114
|
+
// transportLiveAt: fold the server-stamped lastSeen as the FLOOR. The runner ships the max of
|
|
115
|
+
// its own frame/heartbeat-ack timestamps; the server can't expect the runner to know lastSeen,
|
|
116
|
+
// so a fresh heartbeat on EITHER side proves the link is alive. This is the #713/#724 veto:
|
|
117
|
+
// provider-token silence alone never trips stuck-busy while transport is demonstrably live.
|
|
118
|
+
const transportLiveAt = Math.max(liveness?.transportLiveAt ?? 0, agent.lastSeen);
|
|
119
|
+
const transportStaleForMs = Math.max(0, now - transportLiveAt);
|
|
120
|
+
// lastProgressAt floored at the work start so the no-progress age can't predate the turn.
|
|
121
|
+
const lastProgressAt = Math.max(liveness?.lastProgressAt ?? 0, activeWorkStartedAt);
|
|
121
122
|
const staleForMs = Math.max(0, now - lastProgressAt);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
const liveAt = transportLiveAt(agent);
|
|
126
|
-
const transportStaleForMs = Math.max(0, now - liveAt);
|
|
123
|
+
const hasLiveWork = liveness?.hasLiveWork ?? false;
|
|
124
|
+
|
|
125
|
+
// Threshold policy only — no provider knowledge, no raw-signal interpretation.
|
|
127
126
|
const stuck =
|
|
128
|
-
!
|
|
127
|
+
!hasLiveWork &&
|
|
129
128
|
activeWorkAgeMs >= thresholdMs &&
|
|
130
129
|
staleForMs >= thresholdMs &&
|
|
131
130
|
transportStaleForMs >= STUCK_BUSY_TRANSPORT_STALE_MS;
|
|
131
|
+
|
|
132
132
|
const work = classified.work;
|
|
133
133
|
return {
|
|
134
134
|
status: stuck ? "stuck-busy" : "ok",
|
|
135
135
|
kind: classified.kind,
|
|
136
136
|
thresholdMs,
|
|
137
137
|
...(stringValue(work?.id) ? { activeWorkId: stringValue(work?.id) } : {}),
|
|
138
|
-
...(stringValue(work?.kind) ? { activeWorkKind: stringValue(work?.kind) } : {}),
|
|
139
138
|
...(stringValue(work?.label) ? { activeWorkLabel: stringValue(work?.label) } : {}),
|
|
140
139
|
activeWorkStartedAt,
|
|
141
140
|
activeWorkAgeMs,
|
|
142
|
-
...(lastProviderEventAt !== undefined ? { lastProviderEventAt, lastProviderEventAgeMs: Math.max(0, now - lastProviderEventAt) } : {}),
|
|
143
141
|
lastProgressAt,
|
|
144
|
-
transportLiveAt
|
|
142
|
+
transportLiveAt,
|
|
145
143
|
transportStaleForMs,
|
|
144
|
+
hasLiveWork,
|
|
146
145
|
staleForMs,
|
|
147
146
|
};
|
|
148
147
|
}
|
package/src/codex-wham-quota.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { resolveStableCodexQuotaIdentity } from "agent-relay-sdk/provider-quota";
|
|
5
|
-
import { quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
|
|
4
|
+
import { codexWhamResetCredits, codexWhamUsageQuotaState, resolveStableCodexQuotaIdentity } from "agent-relay-sdk/provider-quota";
|
|
6
5
|
import { errMessage, isRecord, stringValue } from "agent-relay-sdk";
|
|
7
6
|
import { codexWhamQuotaEnabled } from "./config";
|
|
8
7
|
import { upsertProviderQuota } from "./db";
|
|
9
8
|
import { emitRelayEvent } from "./events";
|
|
10
9
|
import { getProviderQuotaConfig } from "./provider-quota-config-store";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
10
|
+
import type { ProviderQuotaError } from "./types";
|
|
11
|
+
|
|
12
|
+
// Re-export for callers that import the parsing functions from this module (e.g. tests).
|
|
13
|
+
export { codexWhamUsageQuotaState, codexWhamResetCredits } from "agent-relay-sdk/provider-quota";
|
|
13
14
|
|
|
14
15
|
const WHAM_API_BASE = "https://chatgpt.com/backend-api";
|
|
15
16
|
const WHAM_USAGE_PATH = "/wham/usage";
|
|
@@ -75,43 +76,6 @@ export async function collectCodexWhamQuota(options: {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
export function codexWhamUsageQuotaState(payload: unknown, now = Date.now()): QuotaState | undefined {
|
|
79
|
-
if (!isRecord(payload) || !isRecord(payload.rate_limit)) return undefined;
|
|
80
|
-
const windows = [
|
|
81
|
-
codexWhamWindow("primary", payload.rate_limit.primary_window, now),
|
|
82
|
-
codexWhamWindow("secondary", payload.rate_limit.secondary_window, now),
|
|
83
|
-
].filter((window): window is NonNullable<ContextProbeMetrics["quotaWindows"]>[number] => Boolean(window));
|
|
84
|
-
if (windows.length !== 2) return undefined;
|
|
85
|
-
const quota = quotaStateFromProbeMetrics({
|
|
86
|
-
agentId: "codex-wham-quota",
|
|
87
|
-
contextPercent: 0,
|
|
88
|
-
quotaWindows: windows,
|
|
89
|
-
source: "api",
|
|
90
|
-
confidence: "reported",
|
|
91
|
-
timestamp: now,
|
|
92
|
-
});
|
|
93
|
-
// quotaStateFromProbeMetrics always maps source to "probe"; override to "ingest" so the
|
|
94
|
-
// per-window source precedence in mergeQuotaWindows knows WHAM is authoritative over probe.
|
|
95
|
-
return quota ? { ...quota, source: "ingest" as const } : undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function codexWhamResetCredits(usagePayload: unknown, creditEventsPayload: unknown, now = Date.now()): CodexResetCredits | undefined {
|
|
99
|
-
if (!isRecord(usagePayload) || !isRecord(usagePayload.credits)) return undefined;
|
|
100
|
-
const balance = numberValue(usagePayload.credits.balance);
|
|
101
|
-
if (balance === undefined) return undefined;
|
|
102
|
-
const events = isRecord(creditEventsPayload) && Array.isArray(creditEventsPayload.data)
|
|
103
|
-
? creditEventsPayload.data.length
|
|
104
|
-
: undefined;
|
|
105
|
-
return {
|
|
106
|
-
balance,
|
|
107
|
-
...(typeof usagePayload.credits.has_credits === "boolean" ? { hasCredits: usagePayload.credits.has_credits } : {}),
|
|
108
|
-
...(typeof usagePayload.credits.unlimited === "boolean" ? { unlimited: usagePayload.credits.unlimited } : {}),
|
|
109
|
-
...(typeof usagePayload.credits.overage_limit_reached === "boolean" ? { overageLimitReached: usagePayload.credits.overage_limit_reached } : {}),
|
|
110
|
-
...(events !== undefined ? { creditUsageEvents: events } : {}),
|
|
111
|
-
updatedAt: now,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
79
|
function defaultCodexAuthPath(): string {
|
|
116
80
|
return join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json");
|
|
117
81
|
}
|
|
@@ -149,41 +113,6 @@ async function fetchWhamJson(path: string, auth: CodexWhamAuth, fetchImpl: typeo
|
|
|
149
113
|
return response.json();
|
|
150
114
|
}
|
|
151
115
|
|
|
152
|
-
function codexWhamWindow(name: "primary" | "secondary", value: unknown, now: number): NonNullable<ContextProbeMetrics["quotaWindows"]>[number] | undefined {
|
|
153
|
-
if (!isRecord(value)) return undefined;
|
|
154
|
-
const used = numberValue(value.used_percent);
|
|
155
|
-
if (used === undefined) return undefined;
|
|
156
|
-
const resetsAt = resetAtMs(value.reset_at) ?? resetAfterSecondsToMs(value.reset_after_seconds, now);
|
|
157
|
-
return {
|
|
158
|
-
name,
|
|
159
|
-
utilization: used > 1 ? used / 100 : used,
|
|
160
|
-
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
161
|
-
unit: "%",
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function resetAtMs(value: unknown): number | undefined {
|
|
166
|
-
const number = numberValue(value);
|
|
167
|
-
if (number !== undefined) return number < 10_000_000_000 ? number * 1000 : number;
|
|
168
|
-
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
169
|
-
const parsed = Date.parse(value);
|
|
170
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resetAfterSecondsToMs(value: unknown, now: number): number | undefined {
|
|
174
|
-
const seconds = numberValue(value);
|
|
175
|
-
return seconds === undefined ? undefined : now + Math.max(0, seconds) * 1000;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function numberValue(value: unknown): number | undefined {
|
|
179
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
180
|
-
if (typeof value === "string" && value.trim()) {
|
|
181
|
-
const parsed = Number(value);
|
|
182
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
183
|
-
}
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
116
|
class CodexWhamHttpError extends Error {
|
|
188
117
|
constructor(readonly status: number) {
|
|
189
118
|
super(`Codex WHAM quota endpoint returned HTTP ${status}`);
|
package/src/compaction-watch.ts
CHANGED
|
@@ -39,9 +39,9 @@ function commandKind(type: string): WatchKind | undefined {
|
|
|
39
39
|
return undefined;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
return typeof
|
|
42
|
+
function agentHasTmuxSession(agent: AgentCard | null): boolean {
|
|
43
|
+
const session = agent?.meta?.tmuxSession;
|
|
44
|
+
return typeof session === "string" && session.trim().length > 0;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export class CompactionWatch {
|
|
@@ -70,13 +70,14 @@ export class CompactionWatch {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Arm a watch when a context command's keystrokes are confirmed delivered.
|
|
73
|
-
// Only
|
|
74
|
-
//
|
|
73
|
+
// Only agents with a tmux session go through the blind keystroke-inject path;
|
|
74
|
+
// API-based providers (Codex app-server) compact reliably without this guard.
|
|
75
|
+
// The adapter exposes the tmux-inject path implicitly via meta.tmuxSession (#743).
|
|
75
76
|
noteCommandSucceeded(type: string, commandId: string, target: string): void {
|
|
76
77
|
const kind = commandKind(type);
|
|
77
78
|
if (!kind) return;
|
|
78
79
|
const agent = getAgent(target);
|
|
79
|
-
if (
|
|
80
|
+
if (!agentHasTmuxSession(agent)) return;
|
|
80
81
|
const armedAt = this.now();
|
|
81
82
|
this.pending.set(target, { agentId: target, commandId, kind, armedAt, deadline: armedAt + this.stallMs });
|
|
82
83
|
}
|
|
@@ -4,24 +4,12 @@ const QUOTA_BURN_MIN_SAMPLE_SPAN_MS = 10 * 60_000;
|
|
|
4
4
|
|
|
5
5
|
type QuotaWindow = QuotaState["windows"][number];
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function quotaWindowRole(provider: string, windowName: string): ProviderQuotaBurnRole | undefined {
|
|
12
|
-
const normalizedProvider = normalizeProvider(provider);
|
|
13
|
-
if (normalizedProvider === "codex") {
|
|
14
|
-
if (windowName === "primary") return "5h";
|
|
15
|
-
if (windowName === "secondary") return "7d";
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
if (normalizedProvider === "claude") {
|
|
19
|
-
if (windowName === "five_hour") return "5h";
|
|
20
|
-
if (windowName === "seven_day" || windowName === "seven_day_sonnet" || windowName === "seven_day_opus") return "7d";
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
7
|
+
// Provider-agnostic: window role is expressed by name only. Each adapter uses the
|
|
8
|
+
// conventional names (five_hour/primary → 5h, seven_day*/secondary → 7d); the server
|
|
9
|
+
// never branches on provider identity to resolve a role (#742 / #725 §4.3).
|
|
10
|
+
function quotaWindowRole(windowName: string): ProviderQuotaBurnRole | undefined {
|
|
23
11
|
if (windowName === "five_hour" || windowName === "primary") return "5h";
|
|
24
|
-
if (windowName === "seven_day" || windowName === "secondary") return "7d";
|
|
12
|
+
if (windowName === "seven_day" || windowName === "seven_day_sonnet" || windowName === "seven_day_opus" || windowName === "secondary") return "7d";
|
|
25
13
|
return undefined;
|
|
26
14
|
}
|
|
27
15
|
|
|
@@ -58,7 +46,7 @@ function deriveProviderQuotaBurnWindow(input: {
|
|
|
58
46
|
now: number;
|
|
59
47
|
resetBoundaryToleranceMs: number;
|
|
60
48
|
}): ProviderQuotaBurnWindow | undefined {
|
|
61
|
-
const role = quotaWindowRole(input.
|
|
49
|
+
const role = quotaWindowRole(input.currentWindow.name);
|
|
62
50
|
if (!role) return undefined;
|
|
63
51
|
const low = lowConfidenceBurnWindow(role, input.currentWindow);
|
|
64
52
|
const resetAt = finiteResetBoundary(input.currentWindow);
|