agent-relay-server 0.78.8 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/docs/openapi.json +99 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-NWZehv71.js → MessageBubble-C-3IcqgU.js} +2 -2
  4. package/public/assets/{MessageBubble-NWZehv71.js.map → MessageBubble-C-3IcqgU.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-Dt3AZz4B.js → PlanGraphPanel-o3u36Del.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-Dt3AZz4B.js.map → PlanGraphPanel-o3u36Del.js.map} +1 -1
  7. package/public/assets/{activity-Bo5DgiCq.js → activity-L3POfhGa.js} +2 -2
  8. package/public/assets/{activity-Bo5DgiCq.js.map → activity-L3POfhGa.js.map} +1 -1
  9. package/public/assets/{agent-profiles-_cAkMjLb.js → agent-profiles-CeoqyRis.js} +2 -2
  10. package/public/assets/{agent-profiles-_cAkMjLb.js.map → agent-profiles-CeoqyRis.js.map} +1 -1
  11. package/public/assets/{agents-DWSugBVH.js → agents-DAdDtQb6.js} +2 -2
  12. package/public/assets/{agents-DWSugBVH.js.map → agents-DAdDtQb6.js.map} +1 -1
  13. package/public/assets/{analytics-DLt_3X2u.js → analytics-B6wGSTf3.js} +2 -2
  14. package/public/assets/{analytics-DLt_3X2u.js.map → analytics-B6wGSTf3.js.map} +1 -1
  15. package/public/assets/{automation-CeWlkojL.js → automation-kakk3xZ9.js} +2 -2
  16. package/public/assets/{automation-CeWlkojL.js.map → automation-kakk3xZ9.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-DbJzvZw0.js → branch-state-badge-BSwkl9A8.js} +2 -2
  18. package/public/assets/{branch-state-badge-DbJzvZw0.js.map → branch-state-badge-BSwkl9A8.js.map} +1 -1
  19. package/public/assets/{channels-D8T7P8ui.js → channels-BzY-_4Zi.js} +2 -2
  20. package/public/assets/{channels-D8T7P8ui.js.map → channels-BzY-_4Zi.js.map} +1 -1
  21. package/public/assets/{chat-Bt1j5pmY.js → chat-DDlRjwGC.js} +2 -2
  22. package/public/assets/{chat-Bt1j5pmY.js.map → chat-DDlRjwGC.js.map} +1 -1
  23. package/public/assets/{connectors-BTjbCi5q.js → connectors-empwp8UF.js} +2 -2
  24. package/public/assets/{connectors-BTjbCi5q.js.map → connectors-empwp8UF.js.map} +1 -1
  25. package/public/assets/display-DHgdn4vN.js.map +1 -1
  26. package/public/assets/{formatted-body-impl-gaw1gO1I.js → formatted-body-impl-CEqSWNRL.js} +2 -2
  27. package/public/assets/{formatted-body-impl-gaw1gO1I.js.map → formatted-body-impl-CEqSWNRL.js.map} +1 -1
  28. package/public/assets/{index-CgHPA8OX.js → index-CtGaz-Yg.js} +6 -6
  29. package/public/assets/index-CtGaz-Yg.js.map +1 -0
  30. package/public/assets/{insights-CPQpfNUK.js → insights-mf5sd483.js} +2 -2
  31. package/public/assets/{insights-CPQpfNUK.js.map → insights-mf5sd483.js.map} +1 -1
  32. package/public/assets/{integrations-JEkQvc-S.js → integrations-4V9lOQ8b.js} +2 -2
  33. package/public/assets/{integrations-JEkQvc-S.js.map → integrations-4V9lOQ8b.js.map} +1 -1
  34. package/public/assets/{maintenance-D6c_hvMO.js → maintenance-CruMfiA7.js} +2 -2
  35. package/public/assets/{maintenance-D6c_hvMO.js.map → maintenance-CruMfiA7.js.map} +1 -1
  36. package/public/assets/{managed-agents-DmSmdhRe.js → managed-agents-fOhHGzNI.js} +2 -2
  37. package/public/assets/{managed-agents-DmSmdhRe.js.map → managed-agents-fOhHGzNI.js.map} +1 -1
  38. package/public/assets/{markdown-preview-impl-DbE1Axo2.js → markdown-preview-impl-g5-PTM1A.js} +2 -2
  39. package/public/assets/{markdown-preview-impl-DbE1Axo2.js.map → markdown-preview-impl-g5-PTM1A.js.map} +1 -1
  40. package/public/assets/{memory-B7SuFMg_.js → memory-CRyq2WNC.js} +2 -2
  41. package/public/assets/{memory-B7SuFMg_.js.map → memory-CRyq2WNC.js.map} +1 -1
  42. package/public/assets/{messages-C19vhh1D.js → messages-BqLp0X3y.js} +2 -2
  43. package/public/assets/{messages-C19vhh1D.js.map → messages-BqLp0X3y.js.map} +1 -1
  44. package/public/assets/{orchestrators-Dw2QF8-1.js → orchestrators-BD4nZcG3.js} +2 -2
  45. package/public/assets/{orchestrators-Dw2QF8-1.js.map → orchestrators-BD4nZcG3.js.map} +1 -1
  46. package/public/assets/{overview-DRDCQYId.js → overview-De-2GN06.js} +2 -2
  47. package/public/assets/{overview-DRDCQYId.js.map → overview-De-2GN06.js.map} +1 -1
  48. package/public/assets/{pairs-CeBqU3ev.js → pairs-DOqjBd0R.js} +2 -2
  49. package/public/assets/{pairs-CeBqU3ev.js.map → pairs-DOqjBd0R.js.map} +1 -1
  50. package/public/assets/{projects-CoJjeTUE.js → projects-Byp_gLoD.js} +2 -2
  51. package/public/assets/{projects-CoJjeTUE.js.map → projects-Byp_gLoD.js.map} +1 -1
  52. package/public/assets/{security-2o1oGQ9C.js → security-DtoRSpIi.js} +2 -2
  53. package/public/assets/{security-2o1oGQ9C.js.map → security-DtoRSpIi.js.map} +1 -1
  54. package/public/assets/{select-CYX4uPPQ.js → select-esP750zQ.js} +2 -2
  55. package/public/assets/{select-CYX4uPPQ.js.map → select-esP750zQ.js.map} +1 -1
  56. package/public/assets/{settings-Dk8JFQ4C.js → settings-U8788lYt.js} +2 -2
  57. package/public/assets/{settings-Dk8JFQ4C.js.map → settings-U8788lYt.js.map} +1 -1
  58. package/public/assets/{store-m7GK-CMC.js → store-Cjcd0egy.js} +2 -2
  59. package/public/assets/store-Cjcd0egy.js.map +1 -0
  60. package/public/assets/{tasks-ByWEnbEl.js → tasks-CtiwQy9I.js} +2 -2
  61. package/public/assets/{tasks-ByWEnbEl.js.map → tasks-CtiwQy9I.js.map} +1 -1
  62. package/public/assets/{teams-KsfHMaNk.js → teams-Cx29VUif.js} +2 -2
  63. package/public/assets/{teams-KsfHMaNk.js.map → teams-Cx29VUif.js.map} +1 -1
  64. package/public/assets/{terminal-viewer-impl-BAb6QYYF.js → terminal-viewer-impl-h9hvvB9S.js} +2 -2
  65. package/public/assets/{terminal-viewer-impl-BAb6QYYF.js.map → terminal-viewer-impl-h9hvvB9S.js.map} +1 -1
  66. package/public/assets/{work-queue-DI9rr47X.js → work-queue-C-bS6YmI.js} +2 -2
  67. package/public/assets/{work-queue-DI9rr47X.js.map → work-queue-C-bS6YmI.js.map} +1 -1
  68. package/public/assets/{workspaces-DAuU-uMo.js → workspaces-Bivhqw2N.js} +2 -2
  69. package/public/assets/{workspaces-DAuU-uMo.js.map → workspaces-Bivhqw2N.js.map} +1 -1
  70. package/public/index.html +2 -2
  71. package/src/cli/index.ts +1 -1
  72. package/src/cli/workspace.ts +26 -3
  73. package/src/config-store.ts +1 -1
  74. package/src/db/provider-quotas.ts +3 -1
  75. package/src/quota.ts +1 -1
  76. package/src/routes/index.ts +3 -1
  77. package/src/routes/provider-quotas.ts +4 -1
  78. package/src/routes/workspaces.ts +144 -1
  79. package/public/assets/index-CgHPA8OX.js.map +0 -1
  80. package/public/assets/store-m7GK-CMC.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { isRecord } from "agent-relay-sdk";
2
2
  import { acquireProviderQuotaLease, getOrchestrator, listProviderQuotas, releaseProviderQuotaLease, upsertProviderQuota } from "../db";
3
+ import { emitRelayEvent } from "../events";
3
4
  import { quotaStateFromUnknown, providerQuotaErrorFromUnknown } from "../quota";
4
5
  import { cleanString } from "../validation";
5
6
  import { authorizeRoute, error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
@@ -44,7 +45,9 @@ export const postProviderQuotaRoute: Handler = async (req) => {
44
45
  if (!parsed.ok) return error(parsed.error, parsed.status);
45
46
  const input = cleanProviderQuotaUpdate(parsed.body);
46
47
  if (typeof input === "string") return error(input);
47
- return json(upsertProviderQuota(input), 201);
48
+ const record = upsertProviderQuota(input);
49
+ emitRelayEvent({ type: "provider-quota.updated", source: "server", subject: `${record.provider}:${record.accountKey}`, data: {} });
50
+ return json(record, 201);
48
51
  };
49
52
 
50
53
  function cleanLeaseBody(body: unknown): {
@@ -10,7 +10,7 @@ import { createCommand } from "../commands-db";
10
10
  import { LAND_STRATEGIES, isOwnerAlive, withOwnerOnline } from "../workspace-merge";
11
11
  import { isPathWithinBase } from "../utils";
12
12
  import { resolve } from "node:path";
13
- import { type WorkspaceAutoMergePolicy, type WorkspaceDiagnostics, type WorkspaceGitState, type WorkspaceMergeStrategy, type WorkspaceRecord, type WorkspaceStatus } from "../types";
13
+ import { type WorkspaceAutoMergePolicy, type WorkspaceDiagnostics, type WorkspaceGitState, type WorkspaceMergeStrategy, type WorkspaceRecoveryBranch, type WorkspaceRecord, type WorkspaceStatus } from "../types";
14
14
  import { AUTO_MERGE_POLICIES } from "../workspace-merge";
15
15
  import { workspaceActiveClaim } from "../workspace-claim";
16
16
  import { relayToken } from "../config";
@@ -82,6 +82,149 @@ export const getWorkspaceOrphans: Handler = async () => {
82
82
  return json(reason ? { orphans, missingWorktrees, reason } : { orphans, missingWorktrees });
83
83
  };
84
84
 
85
+ function workspaceCleanupResult(workspace: WorkspaceRecord): Record<string, unknown> | undefined {
86
+ const result = workspace.metadata.cleanupResult;
87
+ return isRecord(result) ? result : undefined;
88
+ }
89
+
90
+ function workspaceBranchKey(repoRoot: string, branch: string): string {
91
+ return `${repoRoot}\0${branch}`;
92
+ }
93
+
94
+ function latestWorkspaceByBranch(workspaces: WorkspaceRecord[]): Map<string, WorkspaceRecord> {
95
+ const byBranch = new Map<string, WorkspaceRecord>();
96
+ for (const workspace of [...workspaces].sort((a, b) => b.updatedAt - a.updatedAt)) {
97
+ if (!workspace.branch) continue;
98
+ const key = workspaceBranchKey(workspace.repoRoot, workspace.branch);
99
+ if (byBranch.has(key)) continue;
100
+ byBranch.set(key, workspace);
101
+ }
102
+ return byBranch;
103
+ }
104
+
105
+ function owningOrchestratorForRepo(repoRoot: string) {
106
+ return listOrchestrators().find((candidate) => candidate.status === "online" && candidate.apiUrl && isPathWithinBase(repoRoot, candidate.baseDir));
107
+ }
108
+
109
+ async function fetchRecoveryBranchesForRepo(repoRoot: string, opts: { baseRef?: string; baseSha?: string } = {}): Promise<{ branches: WorkspaceRecoveryBranch[]; unavailable?: string }> {
110
+ const orch = owningOrchestratorForRepo(repoRoot);
111
+ if (!orch?.apiUrl) return { branches: [], unavailable: "owning orchestrator offline" };
112
+ const query = new URLSearchParams({ repoRoot });
113
+ if (opts.baseRef) query.set("baseRef", opts.baseRef);
114
+ if (opts.baseSha) query.set("baseSha", opts.baseSha);
115
+ const headers: Record<string, string> = {};
116
+ const token = relayToken();
117
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
118
+ try {
119
+ const res = await fetch(`${orch.apiUrl}/api/workspace/recovery-branches?${query.toString()}`, { headers, signal: AbortSignal.timeout(10_000) });
120
+ if (!res.ok) return { branches: [], unavailable: `host returned ${res.status}` };
121
+ const payload = await res.json() as unknown;
122
+ if (!isRecord(payload) || !Array.isArray(payload.branches)) return { branches: [], unavailable: "host returned malformed recovery branch list" };
123
+ return { branches: payload.branches.filter(isRecord) as unknown as WorkspaceRecoveryBranch[] };
124
+ } catch (e) {
125
+ return { branches: [], unavailable: `orchestrator unreachable: ${(e as Error).message}` };
126
+ }
127
+ }
128
+
129
+ export const getWorkspaceRecoveryBranches: Handler = async (req) => {
130
+ try {
131
+ const url = new URL(req.url);
132
+ const repoRoot = cleanString(url.searchParams.get("repoRoot") ?? undefined, "repoRoot", { max: 1000 });
133
+ const baseRef = cleanString(url.searchParams.get("baseRef") ?? undefined, "baseRef", { max: 240 });
134
+ const baseSha = cleanString(url.searchParams.get("baseSha") ?? undefined, "baseSha", { max: 80 });
135
+ const includeActive = url.searchParams.get("includeActive") === "1";
136
+ const allWorkspaces = listWorkspaces(repoRoot ? { repoRoot } : {});
137
+ const repoRoots = repoRoot ? [repoRoot] : [...new Set(allWorkspaces.map((workspace) => workspace.repoRoot))];
138
+ const byBranch = latestWorkspaceByBranch(allWorkspaces);
139
+ const branches: WorkspaceRecoveryBranch[] = [];
140
+ const unavailable: Record<string, string> = {};
141
+
142
+ for (const repo of repoRoots) {
143
+ const fetched = await fetchRecoveryBranchesForRepo(repo, { baseRef, baseSha });
144
+ if (fetched.unavailable) {
145
+ unavailable[repo] = fetched.unavailable;
146
+ continue;
147
+ }
148
+ for (const branch of fetched.branches) {
149
+ const workspace = byBranch.get(workspaceBranchKey(repo, branch.branch));
150
+ const activeWorkspace = Boolean(workspace && !TERMINAL_WORKSPACE_STATUSES.has(workspace.status));
151
+ if (activeWorkspace && !includeActive) continue;
152
+ const owner = workspace?.ownerAgentId ? getAgent(workspace.ownerAgentId) : null;
153
+ const cleanupResult = workspace ? workspaceCleanupResult(workspace) : undefined;
154
+ const cleanupPreservedReason = cleanString(cleanupResult?.branchPreservedReason, "branchPreservedReason", { max: 400 });
155
+ const cleanupCommandId = cleanString(workspace?.metadata.cleanupCommandId, "cleanupCommandId", { max: 160 });
156
+ branches.push({
157
+ ...branch,
158
+ ...(workspace ? { workspaceId: workspace.id } : {}),
159
+ ...(workspace?.ownerAgentId ? { ownerAgentId: workspace.ownerAgentId } : {}),
160
+ ...(owner?.status ? { ownerStatus: owner.status } : {}),
161
+ ...(cleanupCommandId ? { cleanupCommandId } : {}),
162
+ ...(cleanupPreservedReason ? { cleanupPreservedReason, preservedByCleanup: true } : {}),
163
+ ...(activeWorkspace ? { activeWorkspace: true } : {}),
164
+ baseRef: workspace?.baseRef ?? branch.baseRef,
165
+ baseSha: workspace?.baseSha ?? branch.baseSha,
166
+ });
167
+ }
168
+ }
169
+
170
+ branches.sort((a, b) => (b.ageMs ?? 0) - (a.ageMs ?? 0) || a.branch.localeCompare(b.branch));
171
+ return json({ repoRoot, scannedRepos: repoRoots, branches, ...(Object.keys(unavailable).length ? { unavailable } : {}) });
172
+ } catch (e) {
173
+ if (e instanceof ValidationError) return error(e.message, 400);
174
+ throw e;
175
+ }
176
+ };
177
+
178
+ export const postWorkspaceRecoveryBranchDiscard: Handler = async (req) => {
179
+ const denied = authorizeRoute(req, { scope: "command:write" });
180
+ if (denied) return denied;
181
+ const parsed = await parseBody<unknown>(req);
182
+ if (!parsed.ok) return error(parsed.error, parsed.status);
183
+ try {
184
+ if (!isRecord(parsed.body)) return error("body required");
185
+ const repoRoot = cleanString(parsed.body.repoRoot, "repoRoot", { max: 1000 });
186
+ const branch = cleanString(parsed.body.branch, "branch", { max: 240 });
187
+ const baseRef = cleanString(parsed.body.baseRef, "baseRef", { max: 240 });
188
+ const baseSha = cleanString(parsed.body.baseSha, "baseSha", { max: 80 });
189
+ const reason = cleanString(parsed.body.reason, "reason", { max: 1000 });
190
+ const force = parsed.body.force === true;
191
+ if (!repoRoot || !branch) return error("repoRoot and branch required", 400);
192
+ if (!branch.startsWith("agent/")) return error("only agent/* branches can be discarded through recovery cleanup", 400);
193
+ if (force && !reason) return error("reason required for forced recovery branch discard", 400);
194
+ const orch = owningOrchestratorForRepo(repoRoot);
195
+ if (!orch?.agentId) return error("no online orchestrator owns this repo", 409);
196
+ const fetched = await fetchRecoveryBranchesForRepo(repoRoot, { baseRef, baseSha });
197
+ if (fetched.unavailable) return error(fetched.unavailable, 409);
198
+ const target = fetched.branches.find((item) => item.branch === branch);
199
+ if (!target) return error("recovery branch not found", 404);
200
+ if (target.safeToDelete !== true && !force) {
201
+ return error(`branch holds work or cannot be proven safe (${target.preserveReason ?? `${target.unmergedAhead ?? target.ahead ?? "?"} unlanded commit(s)`}); land it first or pass force:true with reason to discard`, 409);
202
+ }
203
+ const command = createCommand({
204
+ type: "workspace.recovery-branch-discard",
205
+ source: "system",
206
+ target: orch.agentId,
207
+ correlationId: branch,
208
+ params: { action: "recovery-branch-discard", repoRoot, branch, baseRef: baseRef ?? target.baseRef, baseSha: baseSha ?? target.baseSha, force, reason, requestedBy: "steward", requestedAt: Date.now() },
209
+ });
210
+ emitCommand(command);
211
+ auditEvent({
212
+ clientId: `workspace-recovery-branch-discard-${Date.now()}`,
213
+ kind: "state",
214
+ title: "Recovery branch discard requested",
215
+ body: force ? `${branch} — forced discard after review: ${reason}` : `${branch} — confirmed empty/superseded`,
216
+ meta: repoRoot,
217
+ icon: "ti-trash",
218
+ view: "orchestrators",
219
+ metadata: { repoRoot, branch, commandId: command.id, force, reason, safeToDelete: target.safeToDelete, preserveReason: target.preserveReason, ...authAuditMetadata(req) },
220
+ });
221
+ return json({ command, branch: target }, 202);
222
+ } catch (e) {
223
+ if (e instanceof ValidationError) return error(e.message, 400);
224
+ throw e;
225
+ }
226
+ };
227
+
85
228
  export const postWorkspaceOrphanReclaim: Handler = async (req) => {
86
229
  const denied = authorizeRoute(req, { scope: "command:write" });
87
230
  if (denied) return denied;