agent-relay-server 0.29.0 → 0.30.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.
- package/package.json +2 -2
- package/public/index.html +218 -69
- package/runner/src/adapter.ts +1 -1
- package/runner/src/config.ts +1 -3
- package/src/agent-branch-state.ts +36 -0
- package/src/agent-ref.ts +18 -1
- package/src/branch-landed.ts +16 -2
- package/src/bus.ts +3 -21
- package/src/db.ts +46 -9
- package/src/maintenance.ts +16 -1
- package/src/mcp.ts +3 -2
- package/src/routes.ts +9 -3
- package/src/sse.ts +15 -27
- package/src/workspace-phase.ts +69 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CONTRIBUTING.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-relay-sdk": "0.2.
|
|
36
|
+
"agent-relay-sdk": "0.2.19"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"prepack": "bun run build:dashboard:bundle >&2",
|
package/public/index.html
CHANGED
|
@@ -11190,6 +11190,17 @@ function shortPath$1(cwd, segments = 2) {
|
|
|
11190
11190
|
const parts = cwd.replace(/\/+$/, "").split("/");
|
|
11191
11191
|
return parts.length <= segments ? cwd : parts.slice(-segments).join("/");
|
|
11192
11192
|
}
|
|
11193
|
+
function normalizePathForCompare(path) {
|
|
11194
|
+
return path.trim().replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
11195
|
+
}
|
|
11196
|
+
function pathWithinBase(path, baseDir) {
|
|
11197
|
+
if (!path?.trim() || !baseDir?.trim()) return false;
|
|
11198
|
+
const normalizedPath = normalizePathForCompare(path);
|
|
11199
|
+
const normalizedBase = normalizePathForCompare(baseDir);
|
|
11200
|
+
if (normalizedBase === "/") return normalizedPath.startsWith("/");
|
|
11201
|
+
if (normalizedPath === "/") return false;
|
|
11202
|
+
return normalizedPath === normalizedBase || normalizedPath.startsWith(normalizedBase + "/");
|
|
11203
|
+
}
|
|
11193
11204
|
function isBuiltInAgent(agent) {
|
|
11194
11205
|
return agent?.meta?.builtin === true || agent?.id === "user" || agent?.id === "system";
|
|
11195
11206
|
}
|
|
@@ -11355,7 +11366,7 @@ function isClaimableTaskWaiting(task) {
|
|
|
11355
11366
|
function isClaimableMessageWaiting(msg) {
|
|
11356
11367
|
return Boolean(msg.claimable && !msg.claimedBy && !(msg.kind === "task" && Number.isSafeInteger(msg.payload?.taskId)));
|
|
11357
11368
|
}
|
|
11358
|
-
function
|
|
11369
|
+
function dashboardTargetMatchesAgent(target, agent) {
|
|
11359
11370
|
if (!target || !agent) return false;
|
|
11360
11371
|
if (target === "broadcast" || target === agent.id) return true;
|
|
11361
11372
|
if (target.startsWith("tag:")) return (agent.tags || []).includes(target.slice(4));
|
|
@@ -11749,7 +11760,7 @@ function timeAgo(now, iso) {
|
|
|
11749
11760
|
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
|
11750
11761
|
return Math.floor(diff / 86400) + "d ago";
|
|
11751
11762
|
}
|
|
11752
|
-
function fmtTime
|
|
11763
|
+
function fmtTime(iso) {
|
|
11753
11764
|
if (!iso) return "";
|
|
11754
11765
|
return new Date(iso).toLocaleString();
|
|
11755
11766
|
}
|
|
@@ -12066,12 +12077,6 @@ function firstAvailableProvider(orchestrator) {
|
|
|
12066
12077
|
function defaultModelFor(provider) {
|
|
12067
12078
|
return PROVIDER_CATALOG[provider]?.defaultModel ?? "";
|
|
12068
12079
|
}
|
|
12069
|
-
function pathWithinBase(path, baseDir) {
|
|
12070
|
-
const value = path.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
12071
|
-
const base = (baseDir || "").trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
12072
|
-
if (!value || !base) return false;
|
|
12073
|
-
return value === base || value.startsWith(base + "/");
|
|
12074
|
-
}
|
|
12075
12080
|
function indexAgents(agents) {
|
|
12076
12081
|
const byId = {};
|
|
12077
12082
|
for (const agent of agents) byId[agent.id] = agent;
|
|
@@ -12191,16 +12196,8 @@ function renderTimelineMarkdown(events, exportedAt) {
|
|
|
12191
12196
|
}
|
|
12192
12197
|
return lines.join("\n");
|
|
12193
12198
|
}
|
|
12194
|
-
function normalizePathForCompare$1(path) {
|
|
12195
|
-
return path.replace(/\/+$/, "") || "/";
|
|
12196
|
-
}
|
|
12197
|
-
function pathWithin$1(path, baseDir) {
|
|
12198
|
-
const normalizedPath = normalizePathForCompare$1(path);
|
|
12199
|
-
const normalizedBase = normalizePathForCompare$1(baseDir);
|
|
12200
|
-
return normalizedPath === normalizedBase || normalizedPath.startsWith(normalizedBase + "/");
|
|
12201
|
-
}
|
|
12202
12199
|
function orchestratorForPath(orchestrators, path) {
|
|
12203
|
-
return orchestrators.filter((orch) => orch.status === "online" &&
|
|
12200
|
+
return orchestrators.filter((orch) => orch.status === "online" && pathWithinBase(path, orch.baseDir)).sort((a, b) => normalizePathForCompare(b.baseDir).length - normalizePathForCompare(a.baseDir).length)[0];
|
|
12204
12201
|
}
|
|
12205
12202
|
function orchestratorForAgent(orchestrators, agent) {
|
|
12206
12203
|
const managed = orchestrators.find((orch) => orch.managedAgents.some((managedAgent) => managedAgent.agentId === agent.id));
|
|
@@ -12293,6 +12290,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12293
12290
|
health: null,
|
|
12294
12291
|
maintenanceJobs: [],
|
|
12295
12292
|
workspaces: [],
|
|
12293
|
+
workspaceFocusId: null,
|
|
12296
12294
|
workspaceGitState: {},
|
|
12297
12295
|
workspaceMergePreview: {},
|
|
12298
12296
|
workspaceDiff: {},
|
|
@@ -12593,6 +12591,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12593
12591
|
if (view === "files") await s.fetchOrchestrators();
|
|
12594
12592
|
if (view === "analytics") await s.fetchAnalytics();
|
|
12595
12593
|
},
|
|
12594
|
+
async openWorkspaceFocus(workspaceId) {
|
|
12595
|
+
set({ workspaceFocusId: workspaceId });
|
|
12596
|
+
await get().switchView("workspaces");
|
|
12597
|
+
},
|
|
12596
12598
|
startClock() {
|
|
12597
12599
|
useClock.getState().start();
|
|
12598
12600
|
},
|
|
@@ -25823,7 +25825,7 @@ function useAgentAttention(agent) {
|
|
|
25823
25825
|
const archivedAt = Number(archivedThreads[agent.id] || 0);
|
|
25824
25826
|
const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25825
25827
|
const pendingPairInvite = pairs.find((p) => (p.status === "active" || p.status === "pending") && (p.requesterId === agent.id || p.targetId === agent.id))?.status === "pending";
|
|
25826
|
-
const claimableTasks = tasks.filter((t) => isClaimableTaskWaiting(t) &&
|
|
25828
|
+
const claimableTasks = tasks.filter((t) => isClaimableTaskWaiting(t) && dashboardTargetMatchesAgent(t.target, agent)).length + messages.filter((m) => isClaimableMessageWaiting(m) && dashboardTargetMatchesAgent(m.to, agent)).length;
|
|
25827
25829
|
return {
|
|
25828
25830
|
unread,
|
|
25829
25831
|
pendingPairInvite,
|
|
@@ -109163,6 +109165,60 @@ function formatTokenCount(tokens) {
|
|
|
109163
109165
|
return String(tokens);
|
|
109164
109166
|
}
|
|
109165
109167
|
//#endregion
|
|
109168
|
+
//#region src/components/shared/branch-state-badge.tsx
|
|
109169
|
+
var BRANCH_STATE_META = {
|
|
109170
|
+
idle: {
|
|
109171
|
+
label: "idle",
|
|
109172
|
+
tip: "Branch worktree — nothing to land yet.",
|
|
109173
|
+
className: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20",
|
|
109174
|
+
Icon: Circle
|
|
109175
|
+
},
|
|
109176
|
+
changes: {
|
|
109177
|
+
label: "changes",
|
|
109178
|
+
tip: "Unlanded work in this branch — use \"Mark workspace ready\" to land it.",
|
|
109179
|
+
className: "bg-amber-500/10 text-amber-400 border-amber-500/20",
|
|
109180
|
+
Icon: GitCommitHorizontal
|
|
109181
|
+
},
|
|
109182
|
+
ready: {
|
|
109183
|
+
label: "ready",
|
|
109184
|
+
tip: "Handed off — the relay auto-merge will land it (~2 min sweep).",
|
|
109185
|
+
className: "bg-blue-500/10 text-blue-400 border-blue-500/20",
|
|
109186
|
+
Icon: CircleArrowUp
|
|
109187
|
+
},
|
|
109188
|
+
steward: {
|
|
109189
|
+
label: "steward",
|
|
109190
|
+
tip: "Under reconciliation — a steward is landing it. No action needed.",
|
|
109191
|
+
className: "bg-orange-500/10 text-orange-400 border-orange-500/20",
|
|
109192
|
+
Icon: Wrench
|
|
109193
|
+
},
|
|
109194
|
+
blocked: {
|
|
109195
|
+
label: "blocked",
|
|
109196
|
+
tip: "Escalated — the auto-merge/steward path is stuck. Needs human attention.",
|
|
109197
|
+
className: "bg-red-500/10 text-red-400 border-red-500/20",
|
|
109198
|
+
Icon: TriangleAlert
|
|
109199
|
+
}
|
|
109200
|
+
};
|
|
109201
|
+
function BranchStateBadge({ state, onClick, iconOnly = false, className }) {
|
|
109202
|
+
const meta = BRANCH_STATE_META[state];
|
|
109203
|
+
const { Icon } = meta;
|
|
109204
|
+
const interactive = Boolean(onClick);
|
|
109205
|
+
const tip = interactive ? `${meta.tip} Click to open this workspace.` : meta.tip;
|
|
109206
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
109207
|
+
role: interactive ? "button" : void 0,
|
|
109208
|
+
tabIndex: interactive ? 0 : void 0,
|
|
109209
|
+
title: tip,
|
|
109210
|
+
onClick,
|
|
109211
|
+
onKeyDown: interactive ? (e) => {
|
|
109212
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
109213
|
+
e.preventDefault();
|
|
109214
|
+
onClick?.(e);
|
|
109215
|
+
}
|
|
109216
|
+
} : void 0,
|
|
109217
|
+
className: cn$2("inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium leading-none", meta.className, interactive && "cursor-pointer hover:brightness-125 focus:outline-none focus:ring-1 focus:ring-current", className),
|
|
109218
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-2.5 w-2.5 shrink-0" }), !iconOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: meta.label })]
|
|
109219
|
+
});
|
|
109220
|
+
}
|
|
109221
|
+
//#endregion
|
|
109166
109222
|
//#region node_modules/@xterm/xterm/lib/xterm.mjs
|
|
109167
109223
|
/**
|
|
109168
109224
|
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
|
|
@@ -127362,16 +127418,8 @@ function parentPath$1(path) {
|
|
|
127362
127418
|
const index = trimmed.lastIndexOf("/");
|
|
127363
127419
|
return index > 0 ? trimmed.slice(0, index) : "/";
|
|
127364
127420
|
}
|
|
127365
|
-
function normalizePathForCompare(path) {
|
|
127366
|
-
return path.replace(/\/+$/, "") || "/";
|
|
127367
|
-
}
|
|
127368
|
-
function pathWithin(path, baseDir) {
|
|
127369
|
-
const normalizedPath = normalizePathForCompare(path);
|
|
127370
|
-
const normalizedBase = normalizePathForCompare(baseDir);
|
|
127371
|
-
return normalizedPath === normalizedBase || normalizedPath.startsWith(normalizedBase + "/");
|
|
127372
|
-
}
|
|
127373
127421
|
function orchestratorForFilePreview(orchestrators, path) {
|
|
127374
|
-
return orchestrators.filter((orch) => orch.status === "online" &&
|
|
127422
|
+
return orchestrators.filter((orch) => orch.status === "online" && pathWithinBase(path, orch.baseDir)).sort((a, b) => normalizePathForCompare(b.baseDir).length - normalizePathForCompare(a.baseDir).length)[0] || orchestrators.find((orch) => orch.status === "online");
|
|
127375
127423
|
}
|
|
127376
127424
|
function supportsSideFilePreview() {
|
|
127377
127425
|
return typeof window !== "undefined" && window.matchMedia("(min-width: 1280px)").matches;
|
|
@@ -127689,7 +127737,7 @@ function AgentListPanel({ threads, onSelectAgent }) {
|
|
|
127689
127737
|
className: "flex items-center gap-1 shrink-0",
|
|
127690
127738
|
children: [lastActivityAt > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
127691
127739
|
className: "text-[10px] text-muted-foreground",
|
|
127692
|
-
title: fmtTime
|
|
127740
|
+
title: fmtTime(lastActivityAt),
|
|
127693
127741
|
children: timeAgo(now, lastActivityAt)
|
|
127694
127742
|
}), unread > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
|
|
127695
127743
|
className: "bg-red-500 text-white text-xs min-w-[18px] h-[18px] flex items-center justify-center shrink-0 px-1",
|
|
@@ -129204,6 +129252,8 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129204
129252
|
const setReplyDraft = useRelayStore((s) => s.setReplyDraft);
|
|
129205
129253
|
const exportThread = useRelayStore((s) => s.exportThread);
|
|
129206
129254
|
const doAgentAction = useRelayStore((s) => s.doAgentAction);
|
|
129255
|
+
const workspaceAction = useRelayStore((s) => s.workspaceAction);
|
|
129256
|
+
const openWorkspaceFocus = useRelayStore((s) => s.openWorkspaceFocus);
|
|
129207
129257
|
const forkFromAgent = useRelayStore((s) => s.forkFromAgent);
|
|
129208
129258
|
const chatHistoryImports = useRelayStore((s) => s.chatHistoryImports);
|
|
129209
129259
|
const peerHistory = useRelayStore((s) => s.threadHistory[s.selectedInboxThread] ?? NO_THREAD_HISTORY);
|
|
@@ -129552,7 +129602,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129552
129602
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
129553
129603
|
className: "flex h-full min-w-0",
|
|
129554
129604
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
129555
|
-
className: "flex h-full min-w-0 flex-1 flex-col",
|
|
129605
|
+
className: "@container/chat flex h-full min-w-0 flex-1 flex-col",
|
|
129556
129606
|
children: [
|
|
129557
129607
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
129558
129608
|
className: "flex items-center justify-between px-3 md:px-4 py-2.5 md:py-3 border-b border-border shrink-0",
|
|
@@ -129596,11 +129646,16 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129596
129646
|
agent && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentRuntimeChips, {
|
|
129597
129647
|
agent,
|
|
129598
129648
|
compact: true,
|
|
129599
|
-
className: "hidden
|
|
129649
|
+
className: "hidden @4xl/chat:flex shrink-0"
|
|
129600
129650
|
}),
|
|
129601
129651
|
agent?.context && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContextRing, { utilization: agent.context.utilization }),
|
|
129652
|
+
agent?.branchState && agent.branchWorkspaceId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BranchStateBadge, {
|
|
129653
|
+
state: agent.branchState,
|
|
129654
|
+
className: "shrink-0",
|
|
129655
|
+
onClick: () => void openWorkspaceFocus(agent.branchWorkspaceId)
|
|
129656
|
+
}),
|
|
129602
129657
|
thread?.attention.unread ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
|
|
129603
|
-
className: "bg-red-500/20 text-red-400 text-xs shrink-0 hidden
|
|
129658
|
+
className: "bg-red-500/20 text-red-400 text-xs shrink-0 hidden @2xl/chat:flex",
|
|
129604
129659
|
children: [thread.attention.unread, " unread"]
|
|
129605
129660
|
}) : null
|
|
129606
129661
|
]
|
|
@@ -129623,7 +129678,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129623
129678
|
canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129624
129679
|
variant: "ghost",
|
|
129625
129680
|
size: "icon-sm",
|
|
129626
|
-
className: "hidden
|
|
129681
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129627
129682
|
disabled: terminalOpening,
|
|
129628
129683
|
title: "Terminal",
|
|
129629
129684
|
onClick: () => void handleOpenTerminal(),
|
|
@@ -129637,10 +129692,18 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129637
129692
|
onClick: interruptAgent,
|
|
129638
129693
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleStop, { className: "w-3.5 h-3.5" })
|
|
129639
129694
|
}),
|
|
129695
|
+
agent.branchState === "changes" && agent.branchWorkspaceId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129696
|
+
variant: "ghost",
|
|
129697
|
+
size: "icon-sm",
|
|
129698
|
+
className: "hidden @4xl/chat:inline-flex text-amber-400 hover:text-amber-300",
|
|
129699
|
+
title: "Mark workspace ready — hand off to the auto-merge",
|
|
129700
|
+
onClick: () => openConfirm("Mark Workspace Ready", `Mark ${displayName(agent)}'s branch ready to land? The relay auto-merge will rebase and land it.`, () => workspaceAction(agent.branchWorkspaceId, "ready")),
|
|
129701
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Flag, { className: "w-3.5 h-3.5" })
|
|
129702
|
+
}),
|
|
129640
129703
|
canCompact && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129641
129704
|
variant: "ghost",
|
|
129642
129705
|
size: "icon-sm",
|
|
129643
|
-
className: "hidden
|
|
129706
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129644
129707
|
title: "Compact",
|
|
129645
129708
|
onClick: () => openConfirm("Compact Agent", `Compact context for ${displayName(agent)}?`, () => doAgentAction(agent, "compact")),
|
|
129646
129709
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Minimize2, { className: "w-3.5 h-3.5" })
|
|
@@ -129648,7 +129711,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129648
129711
|
canClearContext && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129649
129712
|
variant: "ghost",
|
|
129650
129713
|
size: "icon-sm",
|
|
129651
|
-
className: "hidden
|
|
129714
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129652
129715
|
title: "Clear context",
|
|
129653
129716
|
onClick: () => openConfirm("Clear Context", `Clear context for ${displayName(agent)}?`, () => doAgentAction(agent, "clearContext")),
|
|
129654
129717
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Eraser, { className: "w-3.5 h-3.5" })
|
|
@@ -129656,7 +129719,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129656
129719
|
canShutdown && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129657
129720
|
variant: "ghost",
|
|
129658
129721
|
size: "icon-sm",
|
|
129659
|
-
className: "hidden
|
|
129722
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129660
129723
|
title: "Shutdown",
|
|
129661
129724
|
onClick: () => openConfirm("Shutdown Agent", `Shutdown ${displayName(agent)}?`, () => doAgentAction(agent, "shutdown")),
|
|
129662
129725
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Power, { className: "w-3.5 h-3.5" })
|
|
@@ -129664,7 +129727,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129664
129727
|
agent && typeof agent.meta?.cwd === "string" && agent.meta.cwd && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129665
129728
|
variant: "ghost",
|
|
129666
129729
|
size: "icon-sm",
|
|
129667
|
-
className: "hidden
|
|
129730
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129668
129731
|
title: `Open ${agent.meta.cwd} in Files`,
|
|
129669
129732
|
onClick: () => void openFilesForAgent(agent),
|
|
129670
129733
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderOpen, { className: "w-3.5 h-3.5" })
|
|
@@ -129672,7 +129735,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129672
129735
|
agent && thread && thread.messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129673
129736
|
variant: "ghost",
|
|
129674
129737
|
size: "icon-sm",
|
|
129675
|
-
className: "hidden
|
|
129738
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129676
129739
|
title: "Fork — spawn new agent with this chat history",
|
|
129677
129740
|
onClick: () => forkFromAgent(agent.id, thread.messages),
|
|
129678
129741
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitFork, { className: "w-3.5 h-3.5" })
|
|
@@ -129680,7 +129743,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129680
129743
|
thread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129681
129744
|
variant: "ghost",
|
|
129682
129745
|
size: "icon-sm",
|
|
129683
|
-
className: "hidden
|
|
129746
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129684
129747
|
title: "Mark unread",
|
|
129685
129748
|
onClick: () => markInboxThreadUnread(selectedInboxThread),
|
|
129686
129749
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MailX, { className: "w-3.5 h-3.5" })
|
|
@@ -129688,7 +129751,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129688
129751
|
thread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129689
129752
|
variant: "ghost",
|
|
129690
129753
|
size: "icon-sm",
|
|
129691
|
-
className: "hidden
|
|
129754
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129692
129755
|
title: "Mark read",
|
|
129693
129756
|
onClick: () => markInboxThreadRead(thread.peer, thread.messages),
|
|
129694
129757
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MailOpen, { className: "w-3.5 h-3.5" })
|
|
@@ -129696,7 +129759,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129696
129759
|
exportableThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129697
129760
|
variant: "ghost",
|
|
129698
129761
|
size: "icon-sm",
|
|
129699
|
-
className: "hidden
|
|
129762
|
+
className: "hidden @4xl/chat:inline-flex",
|
|
129700
129763
|
title: "Export thread",
|
|
129701
129764
|
onClick: () => exportThread({
|
|
129702
129765
|
...exportableThread,
|
|
@@ -129709,7 +129772,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129709
129772
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
129710
129773
|
variant: "ghost",
|
|
129711
129774
|
size: "icon-sm",
|
|
129712
|
-
className: "
|
|
129775
|
+
className: "@4xl/chat:hidden",
|
|
129713
129776
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Ellipsis, { className: "w-3.5 h-3.5" })
|
|
129714
129777
|
})
|
|
129715
129778
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuContent, {
|
|
@@ -129724,6 +129787,10 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
129724
129787
|
onClick: interruptAgent,
|
|
129725
129788
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleStop, { className: "w-3.5 h-3.5" }), "Interrupt"]
|
|
129726
129789
|
}),
|
|
129790
|
+
agent.branchState === "changes" && agent.branchWorkspaceId && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
|
|
129791
|
+
onClick: () => openConfirm("Mark Workspace Ready", `Mark ${displayName(agent)}'s branch ready to land? The relay auto-merge will rebase and land it.`, () => workspaceAction(agent.branchWorkspaceId, "ready")),
|
|
129792
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Flag, { className: "w-3.5 h-3.5" }), "Mark Ready"]
|
|
129793
|
+
}),
|
|
129727
129794
|
canCompact && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
|
|
129728
129795
|
onClick: () => openConfirm("Compact Agent", `Compact context for ${displayName(agent)}?`, () => doAgentAction(agent, "compact")),
|
|
129729
129796
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Minimize2, { className: "w-3.5 h-3.5" }), "Compact"]
|
|
@@ -130101,6 +130168,8 @@ function AgentCard({ agent }) {
|
|
|
130101
130168
|
const doAgentAction = useRelayStore((s) => s.doAgentAction);
|
|
130102
130169
|
const doDeleteAgent = useRelayStore((s) => s.doDeleteAgent);
|
|
130103
130170
|
const openFilesForAgent = useRelayStore((s) => s.openFilesForAgent);
|
|
130171
|
+
const workspaceAction = useRelayStore((s) => s.workspaceAction);
|
|
130172
|
+
const openWorkspaceFocus = useRelayStore((s) => s.openWorkspaceFocus);
|
|
130104
130173
|
const { terminalOpen, terminalTarget, terminalOpening, openTerminal: handleOpenTerminal, closeTerminal: handleCloseTerminal } = useAgentTerminal(agent.id);
|
|
130105
130174
|
const pairsMap = usePairsByAgentId();
|
|
130106
130175
|
const rawAttention = useAgentAttention(agent);
|
|
@@ -130152,6 +130221,14 @@ function AgentCard({ agent }) {
|
|
|
130152
130221
|
children: shortPath$1(agent.meta.cwd)
|
|
130153
130222
|
})
|
|
130154
130223
|
]
|
|
130224
|
+
}),
|
|
130225
|
+
agent.branchState && agent.branchWorkspaceId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BranchStateBadge, {
|
|
130226
|
+
state: agent.branchState,
|
|
130227
|
+
className: "mt-0.5 shrink-0",
|
|
130228
|
+
onClick: (e) => {
|
|
130229
|
+
e.stopPropagation();
|
|
130230
|
+
openWorkspaceFocus(agent.branchWorkspaceId);
|
|
130231
|
+
}
|
|
130155
130232
|
})
|
|
130156
130233
|
]
|
|
130157
130234
|
}),
|
|
@@ -130236,6 +130313,14 @@ function AgentCard({ agent }) {
|
|
|
130236
130313
|
onClick: () => void handleOpenTerminal(),
|
|
130237
130314
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Terminal, { className: "w-3 h-3" })
|
|
130238
130315
|
}),
|
|
130316
|
+
agent.branchState === "changes" && agent.branchWorkspaceId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
130317
|
+
size: "icon",
|
|
130318
|
+
variant: "ghost",
|
|
130319
|
+
className: "h-7 w-7 text-amber-400 hover:text-amber-300",
|
|
130320
|
+
title: "Mark workspace ready — hand off to the auto-merge",
|
|
130321
|
+
onClick: () => openConfirm("Mark Workspace Ready", `Mark ${displayName(agent)}'s branch ready to land? The relay auto-merge will rebase and land it.`, () => workspaceAction(agent.branchWorkspaceId, "ready")),
|
|
130322
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Flag, { className: "w-3 h-3" })
|
|
130323
|
+
}),
|
|
130239
130324
|
canCompact && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
130240
130325
|
size: "icon",
|
|
130241
130326
|
variant: "ghost",
|
|
@@ -130572,10 +130657,6 @@ function LogViewer({ orchestratorId, session, lines = 200 }) {
|
|
|
130572
130657
|
}
|
|
130573
130658
|
//#endregion
|
|
130574
130659
|
//#region src/components/views/managed-agents.tsx
|
|
130575
|
-
function fmtTime(value) {
|
|
130576
|
-
if (!value) return "-";
|
|
130577
|
-
return new Date(value).toLocaleString();
|
|
130578
|
-
}
|
|
130579
130660
|
function terminalSessionFor(item, agent) {
|
|
130580
130661
|
const agentSession = agent?.meta?.tmuxSession;
|
|
130581
130662
|
if (typeof agentSession === "string" && agentSession.trim()) return agentSession;
|
|
@@ -131857,11 +131938,20 @@ function WorkspaceRow({ workspace }) {
|
|
|
131857
131938
|
const now = useNow();
|
|
131858
131939
|
const [expanded, setExpanded] = (0, import_react.useState)(false);
|
|
131859
131940
|
const agentsById = useRelayStore((s) => s.agentsById);
|
|
131941
|
+
const focused = useRelayStore((s) => s.workspaceFocusId === workspace.id);
|
|
131860
131942
|
const owner = workspace.ownerAgentId ? agentsById[workspace.ownerAgentId] : void 0;
|
|
131861
131943
|
const steward = workspace.stewardAgentId ? agentsById[workspace.stewardAgentId] : void 0;
|
|
131862
131944
|
const path = workspace.worktreePath || workspace.sourceCwd || workspace.repoRoot;
|
|
131945
|
+
const rowRef = (0, import_react.useRef)(null);
|
|
131946
|
+
(0, import_react.useEffect)(() => {
|
|
131947
|
+
if (focused) rowRef.current?.scrollIntoView({
|
|
131948
|
+
behavior: "smooth",
|
|
131949
|
+
block: "center"
|
|
131950
|
+
});
|
|
131951
|
+
}, [focused]);
|
|
131863
131952
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
131864
|
-
|
|
131953
|
+
ref: rowRef,
|
|
131954
|
+
className: `border-t border-border ${focused ? "bg-primary/5 ring-1 ring-inset ring-primary/40" : ""}`,
|
|
131865
131955
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
131866
131956
|
className: "grid gap-3 px-3 py-3 text-sm lg:grid-cols-[minmax(180px,1.2fr)_minmax(220px,1.5fr)_minmax(160px,1fr)_minmax(170px,auto)]",
|
|
131867
131957
|
children: [
|
|
@@ -132006,6 +132096,14 @@ function RepoGroup({ repoRoot, workspaces }) {
|
|
|
132006
132096
|
function WorkspacesView() {
|
|
132007
132097
|
const [filter, setFilter] = (0, import_react.useState)("active");
|
|
132008
132098
|
const workspaces = useRelayStore((s) => s.workspaces);
|
|
132099
|
+
const set = useRelayStore((s) => s.set);
|
|
132100
|
+
const workspaceFocusId = useRelayStore((s) => s.workspaceFocusId);
|
|
132101
|
+
(0, import_react.useEffect)(() => {
|
|
132102
|
+
if (workspaceFocusId) setFilter("all");
|
|
132103
|
+
}, [workspaceFocusId]);
|
|
132104
|
+
(0, import_react.useEffect)(() => () => {
|
|
132105
|
+
set({ workspaceFocusId: null });
|
|
132106
|
+
}, [set]);
|
|
132009
132107
|
const visibleWorkspaces = (0, import_react.useMemo)(() => filterWorkspaces(workspaces, filter), [workspaces, filter]);
|
|
132010
132108
|
const grouped = (0, import_react.useMemo)(() => groupWorkspaces(visibleWorkspaces), [visibleWorkspaces]);
|
|
132011
132109
|
const liveCount = workspaces.filter((item) => LIVE_STATUSES.has(item.status)).length;
|
|
@@ -133615,7 +133713,7 @@ function MemoryView() {
|
|
|
133615
133713
|
}),
|
|
133616
133714
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
133617
133715
|
className: "text-xs text-muted-foreground",
|
|
133618
|
-
title: fmtTime
|
|
133716
|
+
title: fmtTime(selectedMemory.updatedAt),
|
|
133619
133717
|
children: ["updated ", timeAgo(now, selectedMemory.updatedAt)]
|
|
133620
133718
|
}),
|
|
133621
133719
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
@@ -133806,7 +133904,7 @@ function MemoryRow({ memory, selected, now, onSelect }) {
|
|
|
133806
133904
|
}),
|
|
133807
133905
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
133808
133906
|
className: "text-[10px] text-muted-foreground",
|
|
133809
|
-
title: fmtTime
|
|
133907
|
+
title: fmtTime(memory.updatedAt),
|
|
133810
133908
|
children: timeAgo(now, memory.updatedAt)
|
|
133811
133909
|
}),
|
|
133812
133910
|
memory.accessCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -133956,7 +134054,7 @@ function ActivityView() {
|
|
|
133956
134054
|
}),
|
|
133957
134055
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
133958
134056
|
className: "text-xs text-muted-foreground ml-auto shrink-0",
|
|
133959
|
-
title: fmtTime
|
|
134057
|
+
title: fmtTime(item.ts ?? item.createdAt),
|
|
133960
134058
|
children: timeAgo(now, item.ts ?? item.createdAt)
|
|
133961
134059
|
})
|
|
133962
134060
|
]
|
|
@@ -134119,7 +134217,7 @@ function PairCard({ pair, now, agentName, onAccept, onReject, onMessage, onHangu
|
|
|
134119
134217
|
children: pair.id.slice(-10)
|
|
134120
134218
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
134121
134219
|
className: "text-xs text-muted-foreground",
|
|
134122
|
-
title: fmtTime
|
|
134220
|
+
title: fmtTime(pair.updatedAt),
|
|
134123
134221
|
children: timeAgo(now, pair.updatedAt)
|
|
134124
134222
|
})]
|
|
134125
134223
|
})]
|
|
@@ -134412,7 +134510,7 @@ function MessageRow({ msg, now, agentLabel, agentsById, onReply, onClaim, onRetr
|
|
|
134412
134510
|
}),
|
|
134413
134511
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
134414
134512
|
className: "ml-auto shrink-0",
|
|
134415
|
-
title: fmtTime
|
|
134513
|
+
title: fmtTime(msg.createdAt),
|
|
134416
134514
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
134417
134515
|
className: "opacity-40 mr-1",
|
|
134418
134516
|
children: ["#", msg.id]
|
|
@@ -134822,7 +134920,7 @@ function TaskCard({ task, now, agentName, onClaim, onEvents, onStatusChange }) {
|
|
|
134822
134920
|
})]
|
|
134823
134921
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
134824
134922
|
className: "text-xs text-muted-foreground shrink-0",
|
|
134825
|
-
title: fmtTime
|
|
134923
|
+
title: fmtTime(task.createdAt),
|
|
134826
134924
|
children: ["#", task.id]
|
|
134827
134925
|
})]
|
|
134828
134926
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
@@ -134866,7 +134964,7 @@ function TaskCard({ task, now, agentName, onClaim, onEvents, onStatusChange }) {
|
|
|
134866
134964
|
}),
|
|
134867
134965
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
134868
134966
|
className: "flex items-center gap-1 mt-0.5",
|
|
134869
|
-
title: fmtTime
|
|
134967
|
+
title: fmtTime(task.updatedAt),
|
|
134870
134968
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
134871
134969
|
className: "text-muted-foreground/60",
|
|
134872
134970
|
children: "updated"
|
|
@@ -135449,7 +135547,7 @@ function AutomationCard({ automation, runs, now, selected, onEdit, onRun, onTogg
|
|
|
135449
135547
|
className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
|
|
135450
135548
|
children: [
|
|
135451
135549
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
135452
|
-
title: automation.nextRunAt ? fmtTime
|
|
135550
|
+
title: automation.nextRunAt ? fmtTime(automation.nextRunAt) : void 0,
|
|
135453
135551
|
children: ["next ", automation.nextRunAt ? timeAgo(now, automation.nextRunAt) : "disabled"]
|
|
135454
135552
|
}),
|
|
135455
135553
|
latestRun && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
|
|
@@ -135961,7 +136059,7 @@ function RunHistory({ runs, automations, agents, orchestrators, now, logRunId, o
|
|
|
135961
136059
|
}),
|
|
135962
136060
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
135963
136061
|
className: "text-muted-foreground",
|
|
135964
|
-
title: fmtTime
|
|
136062
|
+
title: fmtTime(run.updatedAt),
|
|
135965
136063
|
children: timeAgo(now, run.updatedAt)
|
|
135966
136064
|
}),
|
|
135967
136065
|
logTarget ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
@@ -155739,14 +155837,14 @@ function MaintenanceCard({ job, now, onRun }) {
|
|
|
155739
155837
|
className: "text-muted-foreground",
|
|
155740
155838
|
children: "Last run"
|
|
155741
155839
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.lastRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
155742
|
-
title: fmtTime
|
|
155840
|
+
title: fmtTime(job.lastRunAt),
|
|
155743
155841
|
children: timeAgo(now, job.lastRunAt)
|
|
155744
155842
|
}) : "never" })] }),
|
|
155745
155843
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
|
|
155746
155844
|
className: "text-muted-foreground",
|
|
155747
155845
|
children: "Next run"
|
|
155748
155846
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.nextRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
155749
|
-
title: fmtTime
|
|
155847
|
+
title: fmtTime(job.nextRunAt),
|
|
155750
155848
|
children: nextRunText(now, job.nextRunAt)
|
|
155751
155849
|
}) : "-" })] }),
|
|
155752
155850
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
|
|
@@ -155817,14 +155915,14 @@ function MaintenanceRow({ job, now, onRun }) {
|
|
|
155817
155915
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
155818
155916
|
className: "px-3 py-3 text-xs text-muted-foreground",
|
|
155819
155917
|
children: job.lastRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
155820
|
-
title: fmtTime
|
|
155918
|
+
title: fmtTime(job.lastRunAt),
|
|
155821
155919
|
children: timeAgo(now, job.lastRunAt)
|
|
155822
155920
|
}) : "never"
|
|
155823
155921
|
}),
|
|
155824
155922
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
155825
155923
|
className: "px-3 py-3 text-xs text-muted-foreground",
|
|
155826
155924
|
children: job.nextRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
155827
|
-
title: fmtTime
|
|
155925
|
+
title: fmtTime(job.nextRunAt),
|
|
155828
155926
|
children: nextRunText(now, job.nextRunAt)
|
|
155829
155927
|
}) : "-"
|
|
155830
155928
|
}),
|
|
@@ -156961,7 +157059,7 @@ function AgentDiagnostics({ agent, orchestrators }) {
|
|
|
156961
157059
|
}),
|
|
156962
157060
|
managedAgent.startedAt && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KV, {
|
|
156963
157061
|
label: "Started",
|
|
156964
|
-
value: fmtTime
|
|
157062
|
+
value: fmtTime(managedAgent.startedAt)
|
|
156965
157063
|
}),
|
|
156966
157064
|
managedOrch && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KV, {
|
|
156967
157065
|
label: "Orchestrator",
|
|
@@ -157636,7 +157734,7 @@ function AgentDetailDrawer() {
|
|
|
157636
157734
|
children: "Created"
|
|
157637
157735
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
157638
157736
|
className: "truncate",
|
|
157639
|
-
children: fmtTime
|
|
157737
|
+
children: fmtTime(agent.createdAt)
|
|
157640
157738
|
})]
|
|
157641
157739
|
}),
|
|
157642
157740
|
agent.machine && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -158098,7 +158196,7 @@ function ActiveMemoryRow({ memory, now, onOpen }) {
|
|
|
158098
158196
|
]
|
|
158099
158197
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
158100
158198
|
className: "shrink-0 text-right text-[10px] text-muted-foreground",
|
|
158101
|
-
title: fmtTime
|
|
158199
|
+
title: fmtTime(memory.lastAccessedAt ?? memory.updatedAt),
|
|
158102
158200
|
children: timeAgo(now, memory.lastAccessedAt ?? memory.updatedAt)
|
|
158103
158201
|
})]
|
|
158104
158202
|
})
|
|
@@ -158224,7 +158322,7 @@ function TimelineRow({ entry, now }) {
|
|
|
158224
158322
|
children: entry.title
|
|
158225
158323
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
158226
158324
|
className: "ml-auto shrink-0 text-[10px] text-muted-foreground",
|
|
158227
|
-
title: fmtTime
|
|
158325
|
+
title: fmtTime(entry.ts),
|
|
158228
158326
|
children: timeAgo(now, entry.ts)
|
|
158229
158327
|
})]
|
|
158230
158328
|
}),
|
|
@@ -158897,7 +158995,7 @@ function TaskEventsModal() {
|
|
|
158897
158995
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
158898
158996
|
className: "text-xs text-muted-foreground mt-1",
|
|
158899
158997
|
children: [
|
|
158900
|
-
fmtTime
|
|
158998
|
+
fmtTime(evt.createdAt),
|
|
158901
158999
|
" · ",
|
|
158902
159000
|
evt.source
|
|
158903
159001
|
]
|
|
@@ -161107,6 +161205,10 @@ if ("serviceWorker" in navigator) {
|
|
|
161107
161205
|
container: card-header / inline-size;
|
|
161108
161206
|
}
|
|
161109
161207
|
|
|
161208
|
+
.\@container\/chat {
|
|
161209
|
+
container: chat / inline-size;
|
|
161210
|
+
}
|
|
161211
|
+
|
|
161110
161212
|
.pointer-events-none {
|
|
161111
161213
|
pointer-events: none;
|
|
161112
161214
|
}
|
|
@@ -164684,6 +164786,10 @@ if ("serviceWorker" in navigator) {
|
|
|
164684
164786
|
animation-play-state: paused;
|
|
164685
164787
|
}
|
|
164686
164788
|
|
|
164789
|
+
.ring-inset {
|
|
164790
|
+
--tw-ring-inset: inset;
|
|
164791
|
+
}
|
|
164792
|
+
|
|
164687
164793
|
.running {
|
|
164688
164794
|
animation-play-state: running;
|
|
164689
164795
|
}
|
|
@@ -165011,6 +165117,10 @@ if ("serviceWorker" in navigator) {
|
|
|
165011
165117
|
}
|
|
165012
165118
|
}
|
|
165013
165119
|
|
|
165120
|
+
.hover\:text-amber-300:hover {
|
|
165121
|
+
color: var(--color-amber-300);
|
|
165122
|
+
}
|
|
165123
|
+
|
|
165014
165124
|
.hover\:text-blue-300:hover {
|
|
165015
165125
|
color: var(--color-blue-300);
|
|
165016
165126
|
}
|
|
@@ -165066,13 +165176,36 @@ if ("serviceWorker" in navigator) {
|
|
|
165066
165176
|
.hover\:opacity-100:hover {
|
|
165067
165177
|
opacity: 1;
|
|
165068
165178
|
}
|
|
165179
|
+
|
|
165180
|
+
.hover\:brightness-125:hover {
|
|
165181
|
+
--tw-brightness: brightness(125%);
|
|
165182
|
+
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
|
|
165183
|
+
}
|
|
165069
165184
|
}
|
|
165070
165185
|
|
|
165071
165186
|
.focus\:bg-accent:focus {
|
|
165072
165187
|
background-color: var(--accent);
|
|
165073
165188
|
}
|
|
165074
165189
|
|
|
165075
|
-
.focus\:text-accent-foreground:focus
|
|
165190
|
+
.focus\:text-accent-foreground:focus {
|
|
165191
|
+
color: var(--accent-foreground);
|
|
165192
|
+
}
|
|
165193
|
+
|
|
165194
|
+
.focus\:ring-1:focus {
|
|
165195
|
+
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
165196
|
+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
165197
|
+
}
|
|
165198
|
+
|
|
165199
|
+
.focus\:ring-current:focus {
|
|
165200
|
+
--tw-ring-color: currentcolor;
|
|
165201
|
+
}
|
|
165202
|
+
|
|
165203
|
+
.focus\:outline-none:focus {
|
|
165204
|
+
--tw-outline-style: none;
|
|
165205
|
+
outline-style: none;
|
|
165206
|
+
}
|
|
165207
|
+
|
|
165208
|
+
:is(.focus\:\*\*\:text-accent-foreground:focus *), :is(.not-data-\[variant\=destructive\]\:focus\:\*\*\:text-accent-foreground:not([data-variant="destructive"]):focus *) {
|
|
165076
165209
|
color: var(--accent-foreground);
|
|
165077
165210
|
}
|
|
165078
165211
|
|
|
@@ -165557,10 +165690,6 @@ if ("serviceWorker" in navigator) {
|
|
|
165557
165690
|
display: block;
|
|
165558
165691
|
}
|
|
165559
165692
|
|
|
165560
|
-
.sm\:flex {
|
|
165561
|
-
display: flex;
|
|
165562
|
-
}
|
|
165563
|
-
|
|
165564
165693
|
.sm\:hidden {
|
|
165565
165694
|
display: none;
|
|
165566
165695
|
}
|
|
@@ -165868,6 +165997,26 @@ if ("serviceWorker" in navigator) {
|
|
|
165868
165997
|
}
|
|
165869
165998
|
}
|
|
165870
165999
|
|
|
166000
|
+
@container chat (min-width: 42rem) {
|
|
166001
|
+
.\@2xl\/chat\:flex {
|
|
166002
|
+
display: flex;
|
|
166003
|
+
}
|
|
166004
|
+
}
|
|
166005
|
+
|
|
166006
|
+
@container chat (min-width: 56rem) {
|
|
166007
|
+
.\@4xl\/chat\:flex {
|
|
166008
|
+
display: flex;
|
|
166009
|
+
}
|
|
166010
|
+
|
|
166011
|
+
.\@4xl\/chat\:hidden {
|
|
166012
|
+
display: none;
|
|
166013
|
+
}
|
|
166014
|
+
|
|
166015
|
+
.\@4xl\/chat\:inline-flex {
|
|
166016
|
+
display: inline-flex;
|
|
166017
|
+
}
|
|
166018
|
+
}
|
|
166019
|
+
|
|
165871
166020
|
.dark\:border-input:is(.dark *) {
|
|
165872
166021
|
border-color: var(--input);
|
|
165873
166022
|
}
|
package/runner/src/adapter.ts
CHANGED
|
@@ -174,7 +174,7 @@ export function profileAllowsRelayFeature(config: RunnerSpawnConfig, feature: ke
|
|
|
174
174
|
|
|
175
175
|
export const RELAY_CONTEXT = `[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present you can stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. CLI fallback: agent-relay /reply <messageId> --stdin < response.md; if a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide`;
|
|
176
176
|
|
|
177
|
-
const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
|
|
177
|
+
export const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
|
|
178
178
|
|
|
179
179
|
function attachmentRefs(message: Message): Record<string, unknown>[] {
|
|
180
180
|
const payloadRefs = message.payload?.attachments;
|
package/runner/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir, hostname } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { stringValue } from "agent-relay-sdk";
|
|
4
|
+
import { DEFAULT_RELAY_URL, stringValue } from "agent-relay-sdk";
|
|
5
5
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
6
6
|
import type { ProviderConfig } from "./adapter";
|
|
7
7
|
|
|
@@ -15,8 +15,6 @@ interface LoadedProviderConfig extends ProviderConfig {
|
|
|
15
15
|
path: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const DEFAULT_RELAY_URL = "http://127.0.0.1:4850";
|
|
19
|
-
|
|
20
18
|
function agentRelayHome(): string {
|
|
21
19
|
return process.env.AGENT_RELAY_HOME || join(homedir(), ".agent-relay");
|
|
22
20
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Branch-state enrichment for the agent payload (#236). The single server-side
|
|
2
|
+
// place that hangs the derived `branchState` (+ owning `branchWorkspaceId`) onto an
|
|
3
|
+
// AgentCard before it leaves over the API or SSE — so the dashboard never recomputes
|
|
4
|
+
// it and the projection can't drift between surfaces. The mapping itself lives in
|
|
5
|
+
// deriveBranchState (workspace-phase.ts, pure & CLI-importable); this module is just
|
|
6
|
+
// the agent↔workspace join.
|
|
7
|
+
|
|
8
|
+
import { listWorkspaces, ownedIsolatedWorkspace } from "./db";
|
|
9
|
+
import type { AgentCard, WorkspaceRecord } from "./types";
|
|
10
|
+
import { deriveBranchState, isLiveIsolatedWorkspace } from "./workspace-phase";
|
|
11
|
+
|
|
12
|
+
function withWorkspace(agent: AgentCard, workspace: WorkspaceRecord | undefined): AgentCard {
|
|
13
|
+
const branchState = deriveBranchState(workspace);
|
|
14
|
+
if (!branchState || !workspace) return agent;
|
|
15
|
+
return { ...agent, branchState, branchWorkspaceId: workspace.id };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Enrich a single agent with its branch-state badge fields (one workspace query). */
|
|
19
|
+
export function attachBranchState(agent: AgentCard): AgentCard {
|
|
20
|
+
return withWorkspace(agent, ownedIsolatedWorkspace(agent.id));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Batched enrichment for list endpoints: one workspace scan, mapped by owner, so a
|
|
25
|
+
* fleet of N agents costs a single query instead of N. listWorkspaces is ORDER BY
|
|
26
|
+
* updated_at DESC, so the first live isolated workspace seen per owner is the most
|
|
27
|
+
* recent one — same selection as ownedIsolatedWorkspace.
|
|
28
|
+
*/
|
|
29
|
+
export function attachBranchStates(agents: AgentCard[]): AgentCard[] {
|
|
30
|
+
const byOwner = new Map<string, WorkspaceRecord>();
|
|
31
|
+
for (const ws of listWorkspaces()) {
|
|
32
|
+
if (!ws.ownerAgentId || !isLiveIsolatedWorkspace(ws)) continue;
|
|
33
|
+
if (!byOwner.has(ws.ownerAgentId)) byOwner.set(ws.ownerAgentId, ws);
|
|
34
|
+
}
|
|
35
|
+
return agents.map((agent) => withWorkspace(agent, byOwner.get(agent.id)));
|
|
36
|
+
}
|
package/src/agent-ref.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// never silently picks among several — it reports candidates instead.
|
|
13
13
|
|
|
14
14
|
import { STALE_TTL_MS } from "./config";
|
|
15
|
-
import type { AgentCard } from "./types";
|
|
15
|
+
import type { AgentCard, Message } from "./types";
|
|
16
16
|
|
|
17
17
|
interface ResolveOptions {
|
|
18
18
|
/** Exclude this agent id from matches (e.g. the requester, when pairing). */
|
|
@@ -121,6 +121,23 @@ export function resolveAgentRef(ref: string, agents: AgentCard[], opts: ResolveO
|
|
|
121
121
|
return { status: "not_found", offlineMatches: matches };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export function targetMatchesAgent(target: string, agentId: string, agent?: AgentCard | null): boolean {
|
|
125
|
+
if (!agent) return false;
|
|
126
|
+
if (target === agentId || target === "broadcast") return true;
|
|
127
|
+
if (target.startsWith("tag:") && agent.tags.includes(target.slice(4))) return true;
|
|
128
|
+
if (target.startsWith("cap:") && agent.capabilities.includes(target.slice(4))) return true;
|
|
129
|
+
if (target.startsWith("label:") && agent.label === target.slice(6)) return true;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function messageMatchesAgent(msg: Message, agentId: string, agent?: AgentCard | null): boolean {
|
|
134
|
+
if (!agent) return false;
|
|
135
|
+
if (msg.claimable && msg.claimedBy && msg.claimedBy !== agentId) return false;
|
|
136
|
+
if (msg.resolvedToAgent === agentId) return true;
|
|
137
|
+
if (msg.to === agentId || msg.from === agentId) return true;
|
|
138
|
+
return targetMatchesAgent(msg.to, agentId, agent);
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
// --- messaging send planning -------------------------------------------------
|
|
125
142
|
|
|
126
143
|
export interface DeliveryReceipt {
|
package/src/branch-landed.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface BranchLandedInput {
|
|
|
18
18
|
subject?: string;
|
|
19
19
|
/** Fresh branch the worktree was recycled onto (land-and-continue), if any. */
|
|
20
20
|
newBranch?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the advanced base was pushed to its remote. `false` means the land is
|
|
23
|
+
* local-only (push disabled, or the base has no upstream) and has NOT reached
|
|
24
|
+
* `origin` yet — the notice says so instead of implying a published merge (#285).
|
|
25
|
+
* `undefined` from older orchestrators that don't report it → message stays generic.
|
|
26
|
+
*/
|
|
27
|
+
pushed?: boolean;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -47,6 +54,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
47
54
|
subject: input.subject,
|
|
48
55
|
author: workspace.ownerAgentId,
|
|
49
56
|
newBranch: input.newBranch,
|
|
57
|
+
pushed: input.pushed,
|
|
50
58
|
},
|
|
51
59
|
});
|
|
52
60
|
|
|
@@ -65,8 +73,14 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
65
73
|
sha: input.mergedSha,
|
|
66
74
|
author,
|
|
67
75
|
newBranch: input.newBranch,
|
|
76
|
+
pushed: input.pushed,
|
|
68
77
|
};
|
|
69
78
|
|
|
79
|
+
// A local-only land (push disabled, or base has no upstream) has NOT reached the
|
|
80
|
+
// remote — say so rather than implying a published merge an agent would report as
|
|
81
|
+
// shipped (#285). `undefined` (older orchestrators) keeps the message generic.
|
|
82
|
+
const publishLabel = input.pushed === false ? ` Not yet pushed to \`origin/${base}\` — local only.` : "";
|
|
83
|
+
|
|
70
84
|
// The branch author cares most — push regardless of online (store-ahead delivers it on
|
|
71
85
|
// next poll if they've moved on, #234). They land-and-continue onto the recycled branch.
|
|
72
86
|
if (author) {
|
|
@@ -76,7 +90,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
76
90
|
: " Worktree reclaimed.";
|
|
77
91
|
notifySystemMessage(author, {
|
|
78
92
|
subject: "Your branch landed",
|
|
79
|
-
body: `✅ ${branchLabel} landed on \`${base}\`${shaLabel}${subjectLabel}.${continueLabel}`,
|
|
93
|
+
body: `✅ ${branchLabel} landed on \`${base}\`${shaLabel}${subjectLabel}.${publishLabel}${continueLabel}`,
|
|
80
94
|
payload,
|
|
81
95
|
replyExpected: false,
|
|
82
96
|
});
|
|
@@ -92,7 +106,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
92
106
|
for (const agent of agentsOnMain(workspace.repoRoot, author)) {
|
|
93
107
|
notifySystemMessage(agent.id, {
|
|
94
108
|
subject: `Merged to ${base}`,
|
|
95
|
-
body: `🔀 ${branchLabel}${authorLabel} merged to \`${base}\`${shaLabel}${subjectLabel}
|
|
109
|
+
body: `🔀 ${branchLabel}${authorLabel} merged to \`${base}\`${shaLabel}${subjectLabel}.${publishLabel}`,
|
|
96
110
|
payload,
|
|
97
111
|
replyExpected: false,
|
|
98
112
|
});
|
package/src/bus.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { emitCommandEvent } from "./command-events";
|
|
|
7
7
|
import { getLifecycleManager } from "./lifecycle-manager";
|
|
8
8
|
import { noteAgentTimelineEvent, noteCompactionCommandCompleted } from "./compaction-watch";
|
|
9
9
|
import { applyCommandToRecipe } from "./recipe-runner";
|
|
10
|
+
import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
|
|
10
11
|
import {
|
|
11
12
|
BusProtocolError,
|
|
12
13
|
parseBusFrame,
|
|
@@ -465,11 +466,11 @@ function broadcastRelayEvent(event: RelayEvent): void {
|
|
|
465
466
|
function connectionWantsEvent(conn: BusConnection, event: RelayEvent): boolean {
|
|
466
467
|
if (!matchesSubscription(conn.subscriptions, event.type)) return false;
|
|
467
468
|
if (event.type === "message.new" && conn.agentId) {
|
|
468
|
-
return messageMatchesAgent(event.data as unknown as Message, conn.agentId);
|
|
469
|
+
return messageMatchesAgent(event.data as unknown as Message, conn.agentId, getAgent(conn.agentId));
|
|
469
470
|
}
|
|
470
471
|
if (event.type.startsWith("task.") && conn.agentId) {
|
|
471
472
|
const target = typeof event.data.target === "string" ? event.data.target : "";
|
|
472
|
-
return targetMatchesAgent(target, conn.agentId);
|
|
473
|
+
return targetMatchesAgent(target, conn.agentId, getAgent(conn.agentId));
|
|
473
474
|
}
|
|
474
475
|
if (event.type.startsWith("command.")) {
|
|
475
476
|
const command = isRecord(event.data.command) ? event.data.command : undefined;
|
|
@@ -490,25 +491,6 @@ function matchesSubscription(subscriptions: Set<string>, eventType: string): boo
|
|
|
490
491
|
return false;
|
|
491
492
|
}
|
|
492
493
|
|
|
493
|
-
function messageMatchesAgent(msg: Message, agentId: string): boolean {
|
|
494
|
-
const agent = getAgent(agentId);
|
|
495
|
-
if (!agent) return false;
|
|
496
|
-
if (msg.claimable && msg.claimedBy && msg.claimedBy !== agentId) return false;
|
|
497
|
-
if (msg.resolvedToAgent === agentId) return true;
|
|
498
|
-
if (msg.to === agentId || msg.from === agentId) return true;
|
|
499
|
-
return targetMatchesAgent(msg.to, agentId, agent);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function targetMatchesAgent(target: string, agentId: string, knownAgent?: AgentCard): boolean {
|
|
503
|
-
const agent = knownAgent ?? getAgent(agentId);
|
|
504
|
-
if (!agent) return false;
|
|
505
|
-
if (target === agentId || target === "broadcast") return true;
|
|
506
|
-
if (target.startsWith("tag:") && agent.tags.includes(target.slice(4))) return true;
|
|
507
|
-
if (target.startsWith("cap:") && agent.capabilities.includes(target.slice(4))) return true;
|
|
508
|
-
if (target.startsWith("label:") && agent.label === target.slice(6)) return true;
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
494
|
function outboxToPayload(event: BusEvent) {
|
|
513
495
|
return {
|
|
514
496
|
seq: event.seq,
|
package/src/db.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk";
|
|
4
4
|
import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "./config.ts";
|
|
5
5
|
import { parseJson } from "./utils";
|
|
6
|
+
import { isLiveIsolatedWorkspace } from "./workspace-phase";
|
|
6
7
|
import {
|
|
7
8
|
CONTRACT_REQUIREMENTS,
|
|
8
9
|
contractCompatibility,
|
|
@@ -5281,20 +5282,24 @@ function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord |
|
|
|
5281
5282
|
// preserved and metadata is merged, not replaced.
|
|
5282
5283
|
const existing = getWorkspace(workspace.id);
|
|
5283
5284
|
const preserveStatus = existing != null && existing.status !== "active";
|
|
5284
|
-
// The branch
|
|
5285
|
-
//
|
|
5286
|
-
// runner keeps re-reporting its spawn-time
|
|
5287
|
-
// recycle returns status to "active" — so
|
|
5288
|
-
// the repoint back to the original branch,
|
|
5289
|
-
// branch and strands the work (vent #62 follow-up).
|
|
5290
|
-
//
|
|
5285
|
+
// The branch and base change ONLY via the relay's own land-and-continue recycle
|
|
5286
|
+
// (setWorkspaceBranch repoints to `<branch>-N` and bumps base_sha; base_ref is fixed
|
|
5287
|
+
// at spawn and never re-targeted). The runner keeps re-reporting its spawn-time
|
|
5288
|
+
// branch/base on every heartbeat, and the recycle returns status to "active" — so
|
|
5289
|
+
// without this the next heartbeat clobbers the repoint back to the original branch,
|
|
5290
|
+
// and the next land targets a deleted branch and strands the work (vent #62 follow-up).
|
|
5291
|
+
// baseRef needs the same pin: a heartbeat carrying a stale/wrong base (e.g. a recycled
|
|
5292
|
+
// `-N` branch when the agent was spawned from inside a managed worktree) would otherwise
|
|
5293
|
+
// overwrite the true base, and the next land advances a local-only branch that never
|
|
5294
|
+
// reaches origin/main (#285). Trust the existing row's branch/base over registration;
|
|
5295
|
+
// only a brand-new row takes the runner's values.
|
|
5291
5296
|
return upsertWorkspace({
|
|
5292
5297
|
id: workspace.id,
|
|
5293
5298
|
repoRoot: workspace.repoRoot,
|
|
5294
5299
|
sourceCwd: workspace.sourceCwd ?? agent.cwd,
|
|
5295
5300
|
worktreePath: workspace.worktreePath,
|
|
5296
5301
|
branch: existing?.branch ?? workspace.branch,
|
|
5297
|
-
baseRef: workspace.baseRef,
|
|
5302
|
+
baseRef: existing?.baseRef ?? workspace.baseRef,
|
|
5298
5303
|
baseSha: existing?.baseSha ?? workspace.baseSha,
|
|
5299
5304
|
mode: workspace.mode,
|
|
5300
5305
|
requestedMode: workspace.requestedMode,
|
|
@@ -5394,6 +5399,14 @@ export function deleteWorkspace(id: string): boolean {
|
|
|
5394
5399
|
return db.query("DELETE FROM workspaces WHERE id = ?").run(id).changes > 0;
|
|
5395
5400
|
}
|
|
5396
5401
|
|
|
5402
|
+
// The agent's current branch worktree: its most recent live (non-terminal) isolated
|
|
5403
|
+
// workspace, or undefined. SINGLE HOME for the agent→workspace link — the MCP
|
|
5404
|
+
// owner-resolver and the #236 branch-state badge both go through here. listWorkspaces
|
|
5405
|
+
// is ORDER BY updated_at DESC, so `.find` returns the most recently active one.
|
|
5406
|
+
export function ownedIsolatedWorkspace(agentId: string): WorkspaceRecord | undefined {
|
|
5407
|
+
return listWorkspaces({ ownerAgentId: agentId }).find(isLiveIsolatedWorkspace);
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5397
5410
|
// Shared-mode rows are pure occupancy markers (no worktree on disk) that only
|
|
5398
5411
|
// mean something while their owner is online. Deletion is normally driven by
|
|
5399
5412
|
// the reaper's onAgentDisappeared hook, but agents also leave via clean
|
|
@@ -5416,6 +5429,26 @@ export function pruneOrphanedSharedWorkspaces(): string[] {
|
|
|
5416
5429
|
})();
|
|
5417
5430
|
}
|
|
5418
5431
|
|
|
5432
|
+
// Late-bound listeners for "a workspace row changed in a way that may change its
|
|
5433
|
+
// owner's branch-state badge" (#236). A hook, not a direct emit, because db.ts is
|
|
5434
|
+
// the lowest layer — sse.ts imports db, so db can't import sse without a cycle. sse
|
|
5435
|
+
// registers a listener at startup that re-emits the owner agent's status over SSE.
|
|
5436
|
+
type WorkspaceChangeListener = (workspace: WorkspaceRecord) => void;
|
|
5437
|
+
const workspaceChangeListeners = new Set<WorkspaceChangeListener>();
|
|
5438
|
+
export function onWorkspaceChange(listener: WorkspaceChangeListener): void {
|
|
5439
|
+
workspaceChangeListeners.add(listener);
|
|
5440
|
+
}
|
|
5441
|
+
export function emitWorkspaceChange(workspace: WorkspaceRecord | null | undefined): void {
|
|
5442
|
+
if (!workspace) return;
|
|
5443
|
+
for (const listener of workspaceChangeListeners) {
|
|
5444
|
+
try {
|
|
5445
|
+
listener(workspace);
|
|
5446
|
+
} catch {
|
|
5447
|
+
// A badge-refresh listener must never break a workspace write.
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5419
5452
|
export function updateWorkspaceStatus(id: string, status: WorkspaceStatus, metadata: Record<string, unknown> = {}): WorkspaceRecord | null {
|
|
5420
5453
|
const existing = getWorkspace(id);
|
|
5421
5454
|
if (!existing) return null;
|
|
@@ -5434,7 +5467,11 @@ export function updateWorkspaceStatus(id: string, status: WorkspaceStatus, metad
|
|
|
5434
5467
|
id,
|
|
5435
5468
|
);
|
|
5436
5469
|
electWorkspaceStewards(existing.repoRoot);
|
|
5437
|
-
|
|
5470
|
+
const updated = getWorkspace(id);
|
|
5471
|
+
// Every status transition can flip the owner's badge (active→ready→steward→merged),
|
|
5472
|
+
// so refresh it regardless of caller. Fires on the row's CURRENT owner.
|
|
5473
|
+
emitWorkspaceChange(updated);
|
|
5474
|
+
return updated;
|
|
5438
5475
|
}
|
|
5439
5476
|
|
|
5440
5477
|
// Repoint a workspace row at a recycled branch after a land-and-continue merge
|
package/src/maintenance.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
pruneOfflineAgents,
|
|
20
20
|
pruneOldMessages,
|
|
21
21
|
deleteWorkspace,
|
|
22
|
+
emitWorkspaceChange,
|
|
22
23
|
pruneOrphanedSharedWorkspaces,
|
|
23
24
|
reapStaleAgents,
|
|
24
25
|
reapStaleOrchestrators,
|
|
@@ -34,7 +35,7 @@ import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "./
|
|
|
34
35
|
import { requestWorkspaceMerge } from "./workspace-merge";
|
|
35
36
|
import { workspaceActiveClaim } from "./workspace-claim";
|
|
36
37
|
import { reapOrphanedWorktrees } from "./workspace-orphans";
|
|
37
|
-
import { READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
|
|
38
|
+
import { deriveBranchState, READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
|
|
38
39
|
import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
39
40
|
import { getStewardConfig } from "./config-store";
|
|
40
41
|
import { ensureRepoSteward } from "./steward";
|
|
@@ -583,6 +584,20 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
583
584
|
|
|
584
585
|
const meta = ws.metadata as Record<string, unknown>;
|
|
585
586
|
|
|
587
|
+
// #236: stash the latest ahead/dirty counts so deriveBranchState can tell idle
|
|
588
|
+
// (⚪) from changes (🟡) for an active worktree — the relay isn't in the git
|
|
589
|
+
// path, so this scan is its only window onto the tree. Only write (and refresh
|
|
590
|
+
// the owner's badge) when the derived state actually flips, to avoid SSE churn
|
|
591
|
+
// every 2 min on unchanged workspaces. Status-changing branches below own their
|
|
592
|
+
// own badge refresh via updateWorkspaceStatus.
|
|
593
|
+
const nextAhead = p.ahead ?? 0;
|
|
594
|
+
const nextDirty = p.dirtyCount ?? 0;
|
|
595
|
+
if (meta.gitAhead !== nextAhead || meta.gitDirtyCount !== nextDirty) {
|
|
596
|
+
const before = deriveBranchState(ws);
|
|
597
|
+
const patched = patchWorkspaceMetadata(ws.id, { gitAhead: nextAhead, gitDirtyCount: nextDirty, gitProbedAt: Date.now() });
|
|
598
|
+
if (patched && deriveBranchState(patched) !== before) emitWorkspaceChange(patched);
|
|
599
|
+
}
|
|
600
|
+
|
|
586
601
|
// Landing wins over everything else. Once the work is in base — whether the
|
|
587
602
|
// PR was squash/cherry-pick merged on GitHub or fast-forwarded locally — the
|
|
588
603
|
// workspace is done, even if `git merge-tree` still predicts a textual
|
package/src/mcp.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
linkArtifact,
|
|
24
24
|
listAgents,
|
|
25
25
|
listWorkspaces,
|
|
26
|
+
ownedIsolatedWorkspace,
|
|
26
27
|
searchAgents,
|
|
27
28
|
type AgentSearchFilter,
|
|
28
29
|
type AgentSearchSort,
|
|
@@ -45,7 +46,7 @@ import {
|
|
|
45
46
|
} from "./security";
|
|
46
47
|
import type { ActivityKind, AgentCard, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider, WorkspaceMergeStrategy, WorkspaceRecord } from "./types";
|
|
47
48
|
import { applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "./workspace-actions";
|
|
48
|
-
import { describeWorkspacePhase, landReceipt, readyContract,
|
|
49
|
+
import { describeWorkspacePhase, landReceipt, readyContract, worktreeMcpInstructions } from "./workspace-phase";
|
|
49
50
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
50
51
|
import { errMessage, isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
|
|
51
52
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
@@ -964,7 +965,7 @@ function resolveWorkspaceForCaller(auth: McpAuthContext, args: Record<string, un
|
|
|
964
965
|
function callerIsolatedWorkspace(auth: McpAuthContext): WorkspaceRecord | undefined {
|
|
965
966
|
const caller = callerAgentId(auth);
|
|
966
967
|
if (!caller) return undefined;
|
|
967
|
-
return
|
|
968
|
+
return ownedIsolatedWorkspace(caller);
|
|
968
969
|
}
|
|
969
970
|
|
|
970
971
|
async function relayWorkspaceStatus(auth: McpAuthContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
package/src/routes.ts
CHANGED
|
@@ -160,6 +160,7 @@ import { getIntegrationTokens, INTEGRATION_RATE_LIMIT_PER_MINUTE, MAX_BODY_BYTES
|
|
|
160
160
|
import { CONTRACT_VERSIONS, parseRuntimeCapabilities, parseRuntimeContracts, parseRuntimePackage, type RuntimeCapabilities, type RuntimeContracts, type RuntimePackageMetadata } from "./contracts";
|
|
161
161
|
import { listHostDirectories } from "./agent-spawn";
|
|
162
162
|
import { planSend } from "./agent-ref";
|
|
163
|
+
import { attachBranchState, attachBranchStates } from "./agent-branch-state";
|
|
163
164
|
import { defaultProviderConfig, loadProviderConfig, providerConfigPublic, writeProviderConfig } from "../runner/src/config";
|
|
164
165
|
import type { ProviderConfig } from "../runner/src/adapter";
|
|
165
166
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
@@ -1327,7 +1328,7 @@ const getAgents: Handler = (req) => {
|
|
|
1327
1328
|
const tag = url.searchParams.get("tag") ?? undefined;
|
|
1328
1329
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
1329
1330
|
const status = url.searchParams.get("status") ?? undefined;
|
|
1330
|
-
return json(listAgents({ tag, machine, status }));
|
|
1331
|
+
return json(attachBranchStates(listAgents({ tag, machine, status })));
|
|
1331
1332
|
};
|
|
1332
1333
|
|
|
1333
1334
|
const findAgents: Handler = (req) => {
|
|
@@ -1361,7 +1362,7 @@ const postRouteAdvice: Handler = async (req) => {
|
|
|
1361
1362
|
|
|
1362
1363
|
const getAgentById: Handler = (_req, params) => {
|
|
1363
1364
|
const agent = getAgent(params.id!);
|
|
1364
|
-
return agent ? json(agent) : error("agent not found", 404);
|
|
1365
|
+
return agent ? json(attachBranchState(agent)) : error("agent not found", 404);
|
|
1365
1366
|
};
|
|
1366
1367
|
|
|
1367
1368
|
const getAgentReplyObligations: Handler = (_req, params) => {
|
|
@@ -4495,8 +4496,12 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4495
4496
|
// Repoint the row so the next merge targets the live branch, not the deleted one.
|
|
4496
4497
|
const newBranch = cleanString(command.result.newBranch, "result.newBranch", { max: 240 });
|
|
4497
4498
|
const mergedSha = cleanString(command.result.mergedSha, "result.mergedSha", { max: 64 });
|
|
4499
|
+
// base_sha tracks the tip the recycled workspace forks from. On a no-ff land
|
|
4500
|
+
// (#287) that's the merge commit (result.baseSha), not the preserved landed
|
|
4501
|
+
// commit (mergedSha); fall back to mergedSha for older orchestrators.
|
|
4502
|
+
const baseSha = cleanString(command.result.baseSha, "result.baseSha", { max: 64 });
|
|
4498
4503
|
if (newBranch) {
|
|
4499
|
-
setWorkspaceBranch(workspaceId, newBranch, mergedSha);
|
|
4504
|
+
setWorkspaceBranch(workspaceId, newBranch, baseSha ?? mergedSha);
|
|
4500
4505
|
}
|
|
4501
4506
|
// #239 — push the author a "your branch landed" notice (no polling). Only on a
|
|
4502
4507
|
// real land; a no-op resolution (#230) merged nothing, so it earns no notice.
|
|
@@ -4506,6 +4511,7 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4506
4511
|
mergedSha,
|
|
4507
4512
|
subject: cleanString(command.result.subject, "result.subject", { max: 200 }),
|
|
4508
4513
|
newBranch,
|
|
4514
|
+
pushed: typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
|
|
4509
4515
|
});
|
|
4510
4516
|
}
|
|
4511
4517
|
}
|
package/src/sse.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { getAgent, getOrchestrator } from "./db";
|
|
1
|
+
import { getAgent, getOrchestrator, onWorkspaceChange } from "./db";
|
|
2
|
+
import { attachBranchState } from "./agent-branch-state";
|
|
2
3
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
4
|
+
import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
|
|
3
5
|
import type { ActivityEvent, AgentCard, Message, RelayNotification, Task } from "./types";
|
|
4
6
|
import { isRecord } from "agent-relay-sdk";
|
|
5
7
|
|
|
@@ -68,18 +70,6 @@ function send(conn: Connection, event: string, data: unknown) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function messageMatchesAgent(msg: Message, agentId: string): boolean {
|
|
72
|
-
const agent = getAgent(agentId);
|
|
73
|
-
if (!agent) return false;
|
|
74
|
-
if (msg.resolvedToAgent === agentId) return true;
|
|
75
|
-
if (msg.to === agentId || msg.from === agentId) return true;
|
|
76
|
-
if (msg.to === "broadcast") return true;
|
|
77
|
-
if (msg.to.startsWith("tag:") && agent.tags.includes(msg.to.slice(4))) return true;
|
|
78
|
-
if (msg.to.startsWith("cap:") && agent.capabilities.includes(msg.to.slice(4))) return true;
|
|
79
|
-
if (msg.to.startsWith("label:") && agent.label === msg.to.slice(6)) return true;
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
73
|
function fanout(event: RelayEvent): void {
|
|
84
74
|
if (event.type === "message.new") {
|
|
85
75
|
sendNewMessage(event.data as unknown as Message);
|
|
@@ -100,8 +90,7 @@ function sendNewMessage(msg: Message): void {
|
|
|
100
90
|
send(conn, "message.new", msg);
|
|
101
91
|
continue;
|
|
102
92
|
}
|
|
103
|
-
if (!messageMatchesAgent(msg, conn.agentId)) continue;
|
|
104
|
-
if (msg.claimable && msg.claimedBy && msg.claimedBy !== conn.agentId) continue;
|
|
93
|
+
if (!messageMatchesAgent(msg, conn.agentId, getAgent(conn.agentId))) continue;
|
|
105
94
|
send(conn, "message.new", msg);
|
|
106
95
|
}
|
|
107
96
|
}
|
|
@@ -114,12 +103,21 @@ export function emitNewMessage(msg: Message) {
|
|
|
114
103
|
|
|
115
104
|
export function emitAgentStatus(agentId: string) {
|
|
116
105
|
const agent = getAgent(agentId);
|
|
117
|
-
|
|
106
|
+
// Enrich with the branch-state badge fields (#236) so the dashboard's live agent
|
|
107
|
+
// updates carry the same projection as GET /api/agents.
|
|
108
|
+
const data = agent ? attachBranchState(agent) : { id: agentId, status: "offline" };
|
|
118
109
|
emitRelayEvent({ type: "agent.status", source: "server", subject: agentId, data: data as unknown as Record<string, unknown> });
|
|
119
110
|
const notification = agent ? notificationForAgentStatus(agent) : undefined;
|
|
120
111
|
if (notification) emitNotificationCreated(notification);
|
|
121
112
|
}
|
|
122
113
|
|
|
114
|
+
// A workspace row changed (status transition or an idle↔changes git snapshot) →
|
|
115
|
+
// re-emit its owner's agent.status so the branch-state badge updates live (#236).
|
|
116
|
+
// Registered against the db hook to avoid a db→sse import cycle.
|
|
117
|
+
onWorkspaceChange((workspace) => {
|
|
118
|
+
if (workspace.ownerAgentId) emitAgentStatus(workspace.ownerAgentId);
|
|
119
|
+
});
|
|
120
|
+
|
|
123
121
|
export function emitAgentRemoved(agentId: string) {
|
|
124
122
|
emitRelayEvent({ type: "agent.removed", source: "server", subject: agentId, data: { id: agentId } });
|
|
125
123
|
}
|
|
@@ -165,7 +163,7 @@ export function emitMessageReactionUpdated(msg: Message) {
|
|
|
165
163
|
|
|
166
164
|
function sendTaskChanged(task: Task, eventType = "task.updated"): void {
|
|
167
165
|
for (const conn of connections.values()) {
|
|
168
|
-
if (conn.agentId && !targetMatchesAgent(task.target, conn.agentId)) continue;
|
|
166
|
+
if (conn.agentId && !targetMatchesAgent(task.target, conn.agentId, getAgent(conn.agentId))) continue;
|
|
169
167
|
send(conn, eventType, task);
|
|
170
168
|
}
|
|
171
169
|
}
|
|
@@ -194,16 +192,6 @@ export function getConnectionCount(): number {
|
|
|
194
192
|
return connections.size;
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
function targetMatchesAgent(target: string, agentId: string): boolean {
|
|
198
|
-
const agent = getAgent(agentId);
|
|
199
|
-
if (!agent) return false;
|
|
200
|
-
if (target === agentId || target === "broadcast") return true;
|
|
201
|
-
if (target.startsWith("tag:") && agent.tags.includes(target.slice(4))) return true;
|
|
202
|
-
if (target.startsWith("cap:") && agent.capabilities.includes(target.slice(4))) return true;
|
|
203
|
-
if (target.startsWith("label:") && agent.label === target.slice(6)) return true;
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
195
|
export function emitOrchestratorStatus(orchestratorId: string) {
|
|
208
196
|
const orch = getOrchestrator(orchestratorId);
|
|
209
197
|
const data = orch ?? { id: orchestratorId, status: "offline" };
|
package/src/workspace-phase.ts
CHANGED
|
@@ -15,13 +15,23 @@
|
|
|
15
15
|
// anti-panic signal.
|
|
16
16
|
|
|
17
17
|
import { TERMINAL_WORKSPACE_STATUS_VALUES } from "agent-relay-sdk";
|
|
18
|
-
import type { WorkspaceRecord, WorkspaceStatus } from "./types";
|
|
18
|
+
import type { BranchState, WorkspaceRecord, WorkspaceStatus } from "./types";
|
|
19
|
+
import { workspaceActiveClaim } from "./workspace-claim";
|
|
19
20
|
|
|
20
21
|
// Statuses where the worktree's lifecycle is over — landed or torn down. Single
|
|
21
22
|
// home; imported by maintenance (stale reap), routes (orphan scan), and the MCP
|
|
22
23
|
// initialize primer (don't brief an agent on a dead workspace). Was duplicated.
|
|
23
24
|
export const TERMINAL_WORKSPACE_STATUSES = new Set<WorkspaceStatus>(TERMINAL_WORKSPACE_STATUS_VALUES);
|
|
24
25
|
|
|
26
|
+
// The "this is the worktree the agent is actively branch-working in" predicate:
|
|
27
|
+
// isolated mode and not yet landed/torn down. SINGLE HOME — the MCP owner-workspace
|
|
28
|
+
// resolver, the db owner lookup, and the #236 badge enrichment all mean the same
|
|
29
|
+
// thing; they drifted into private copies before. listWorkspaces is ORDER BY
|
|
30
|
+
// updated_at DESC, so the first match per owner is the most recent live worktree.
|
|
31
|
+
export function isLiveIsolatedWorkspace(ws: Pick<WorkspaceRecord, "mode" | "status">): boolean {
|
|
32
|
+
return ws.mode === "isolated" && !TERMINAL_WORKSPACE_STATUSES.has(ws.status);
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
// The "handed off, waiting to land" statuses — an agent has finished and the
|
|
26
36
|
// auto-merge-back is responsible for getting the branch onto base. SINGLE HOME:
|
|
27
37
|
// the auto-land consumer (maintenance `autoMergeCleanFastForwards`) and the
|
|
@@ -209,6 +219,64 @@ export function describeWorkspacePhase(
|
|
|
209
219
|
}
|
|
210
220
|
}
|
|
211
221
|
|
|
222
|
+
function metaNumber(meta: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
223
|
+
const v = meta?.[key];
|
|
224
|
+
return typeof v === "number" ? v : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// THE branch-state projection for the human (#236) — the sibling of
|
|
228
|
+
// describeWorkspacePhase, which targets the agent. Maps a workspace to one compact
|
|
229
|
+
// state for the agent-card/chat badge, so the recurring "does this agent have
|
|
230
|
+
// unlanded work, and where is it in the merge-back" question is answered at a
|
|
231
|
+
// glance. SINGLE HOME (server-side); the dashboard never recomputes it.
|
|
232
|
+
//
|
|
233
|
+
// idle/changes need the worktree's ahead/dirty counts, which the relay isn't in the
|
|
234
|
+
// git path to know live — the ~2 min conflict scan stashes them in metadata
|
|
235
|
+
// (`gitAhead`/`gitDirtyCount`). Until the first scan they're absent, so an active
|
|
236
|
+
// worktree shows the optimistic `changes` (the high-value "mark ready" affordance);
|
|
237
|
+
// the scan then settles it to `idle` when genuinely empty (#236 v1 option a).
|
|
238
|
+
//
|
|
239
|
+
// Returns undefined for non-branch / torn-down workspaces (no badge).
|
|
240
|
+
export function deriveBranchState(
|
|
241
|
+
workspace: Pick<WorkspaceRecord, "status" | "metadata" | "readyAt"> | null | undefined,
|
|
242
|
+
opts: { now?: number; stallMs?: number } = {},
|
|
243
|
+
): BranchState | undefined {
|
|
244
|
+
if (!workspace) return undefined;
|
|
245
|
+
const now = opts.now ?? Date.now();
|
|
246
|
+
switch (workspace.status) {
|
|
247
|
+
case "active": {
|
|
248
|
+
const meta = workspace.metadata as Record<string, unknown> | undefined;
|
|
249
|
+
const ahead = metaNumber(meta, "gitAhead");
|
|
250
|
+
const dirty = metaNumber(meta, "gitDirtyCount");
|
|
251
|
+
if (ahead === undefined && dirty === undefined) return "changes";
|
|
252
|
+
return (ahead ?? 0) > 0 || (dirty ?? 0) > 0 ? "changes" : "idle";
|
|
253
|
+
}
|
|
254
|
+
case "ready":
|
|
255
|
+
case "review_requested":
|
|
256
|
+
// Handed off, waiting for the auto-merge. A steward holding the claim means a
|
|
257
|
+
// human-out-of-loop reconciliation is underway → 🟠, otherwise the robot has it → 🔵.
|
|
258
|
+
return workspaceActiveClaim(workspace, now)?.purpose === "steward" ? "steward" : "ready";
|
|
259
|
+
case "merge_planned":
|
|
260
|
+
// Merge dispatched / under reconciliation — robot-or-steward, either way not the human's move.
|
|
261
|
+
return "steward";
|
|
262
|
+
case "conflict": {
|
|
263
|
+
// Held by a steward → reconciling (🟠). Past the stall window with nobody
|
|
264
|
+
// holding it → the steward path isn't progressing → escalate to the human (🔴).
|
|
265
|
+
if (workspaceActiveClaim(workspace, now)?.purpose === "steward") return "steward";
|
|
266
|
+
const stallMs = opts.stallMs ?? LAND_PENDING_STALL_MS;
|
|
267
|
+
const since = typeof workspace.readyAt === "number" ? now - workspace.readyAt : undefined;
|
|
268
|
+
if (since !== undefined && since > stallMs) return "blocked";
|
|
269
|
+
return "steward";
|
|
270
|
+
}
|
|
271
|
+
case "merged":
|
|
272
|
+
case "abandoned":
|
|
273
|
+
case "cleanup_requested":
|
|
274
|
+
case "cleaned":
|
|
275
|
+
// Terminal/torn down: the owner lookup filters these out, so the badge clears.
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
212
280
|
// Plain-language contract printed/returned right when an agent marks a workspace
|
|
213
281
|
// ready, so the whole "what happens next" is stated up front instead of being
|
|
214
282
|
// decoded from status enums over the following minutes (#235).
|