agent-relay-server 0.118.2 → 0.118.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.118.
|
|
5
|
+
"version": "0.118.4",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.118.
|
|
3
|
+
"version": "0.118.4",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"CONTRIBUTING.md"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"agent-relay-channels-host": "0.118.
|
|
39
|
+
"agent-relay-channels-host": "0.118.4",
|
|
40
40
|
"agent-relay-providers": "0.104.1",
|
|
41
41
|
"agent-relay-sdk": "0.2.101",
|
|
42
42
|
"ajv": "^8.20.0"
|
|
@@ -35,10 +35,10 @@ import { renderTemplate } from "./prompt-resolver";
|
|
|
35
35
|
import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
|
|
36
36
|
import { emitWorkspaceTaskSemanticNotifications } from "./services/task-lifecycle";
|
|
37
37
|
import { ensureRepoSteward } from "./steward";
|
|
38
|
-
import { requestWorkspaceMerge } from "./workspace-merge";
|
|
38
|
+
import { requestWorkspaceMerge, workspaceMergeOrchestrators } from "./workspace-merge";
|
|
39
39
|
import { workspaceActiveClaim } from "./workspace-claim";
|
|
40
40
|
import { DIRTY_WORKTREE_LAND_SKIP_REASON, READY_TO_LAND_STATUSES } from "./workspace-phase";
|
|
41
|
-
import { fetchHostMergePreview
|
|
41
|
+
import { fetchHostMergePreview } from "./workspace-probe";
|
|
42
42
|
import type { WorkspaceAutoMergePolicy, WorkspaceLandingPolicy, WorkspaceMergePreview, WorkspaceMergeStrategy, WorkspaceRecord } from "./types";
|
|
43
43
|
import { STEWARD_WAKE_COOLDOWN_MS, stewardFallbackTarget, workspaceAutoMergeEnabled } from "./config";
|
|
44
44
|
|
|
@@ -252,13 +252,29 @@ interface LandSweepResult {
|
|
|
252
252
|
heldByClaim: string[];
|
|
253
253
|
heldByPause: string[];
|
|
254
254
|
heldAwaitingApproval: string[];
|
|
255
|
+
previewUnavailable: string[];
|
|
255
256
|
backedOff: string[];
|
|
256
257
|
leftForSteward: string[];
|
|
257
258
|
wokeStewards: string[];
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
function emptyLandSweep(): LandSweepResult {
|
|
261
|
-
return { scanned: 0, merged: [], heldByLease: [], heldByClaim: [], heldByPause: [], heldAwaitingApproval: [], backedOff: [], leftForSteward: [], wokeStewards: [] };
|
|
262
|
+
return { scanned: 0, merged: [], heldByLease: [], heldByClaim: [], heldByPause: [], heldAwaitingApproval: [], previewUnavailable: [], backedOff: [], leftForSteward: [], wokeStewards: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function fetchFirstUsableMergePreview(
|
|
266
|
+
ws: WorkspaceRecord,
|
|
267
|
+
orchestrators: ReturnType<typeof listOrchestrators>,
|
|
268
|
+
): Promise<WorkspaceMergePreview | { available: false } | null> {
|
|
269
|
+
for (const orch of workspaceMergeOrchestrators(ws, orchestrators)) {
|
|
270
|
+
if (!orch.apiUrl) continue;
|
|
271
|
+
const preview = await fetchHostMergePreview(orch.apiUrl, ws);
|
|
272
|
+
if (!preview || (preview as { available?: false }).available === false) continue;
|
|
273
|
+
const p = preview as WorkspaceMergePreview;
|
|
274
|
+
if (p.error || p.missing) continue;
|
|
275
|
+
return p;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
262
278
|
}
|
|
263
279
|
|
|
264
280
|
// Deterministic auto-land (Layer 0, #167/#207/#242): land conflict-free ready work
|
|
@@ -269,7 +285,7 @@ async function landCandidates(
|
|
|
269
285
|
orchestrators: ReturnType<typeof listOrchestrators>,
|
|
270
286
|
): Promise<LandSweepResult> {
|
|
271
287
|
const stewardEnabled = getStewardConfig().enabled;
|
|
272
|
-
const merged: string[] = [];
|
|
288
|
+
const merged: string[] = [], previewUnavailable: string[] = [];
|
|
273
289
|
const heldByLease: string[] = [], heldByClaim: string[] = [], heldByPause: string[] = [], heldAwaitingApproval: string[] = [], backedOff: string[] = [], leftForSteward: string[] = [], wokeStewards: string[] = [];
|
|
274
290
|
|
|
275
291
|
for (const ws of candidates) {
|
|
@@ -286,12 +302,12 @@ async function landCandidates(
|
|
|
286
302
|
escalateNoProgressMerge(ws);
|
|
287
303
|
continue;
|
|
288
304
|
}
|
|
289
|
-
const
|
|
290
|
-
if (!
|
|
291
|
-
|
|
292
|
-
|
|
305
|
+
const preview = await fetchFirstUsableMergePreview(ws, orchestrators);
|
|
306
|
+
if (!preview) {
|
|
307
|
+
previewUnavailable.push(ws.id);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
293
310
|
const p = preview as WorkspaceMergePreview;
|
|
294
|
-
if (p.error || p.missing) continue;
|
|
295
311
|
if (p.reason === DIRTY_WORKTREE_LAND_SKIP_REASON) {
|
|
296
312
|
patchWorkspaceMetadata(ws.id, { lastLandSkipReason: p.reason, lastLandSkipAt: Date.now() });
|
|
297
313
|
} else if (ws.metadata.lastLandSkipReason === DIRTY_WORKTREE_LAND_SKIP_REASON) {
|
|
@@ -354,7 +370,7 @@ async function landCandidates(
|
|
|
354
370
|
});
|
|
355
371
|
}
|
|
356
372
|
|
|
357
|
-
return { scanned: candidates.length, merged, heldByLease, heldByClaim, heldByPause, heldAwaitingApproval, backedOff, leftForSteward, wokeStewards };
|
|
373
|
+
return { scanned: candidates.length, merged, heldByLease, heldByClaim, heldByPause, heldAwaitingApproval, previewUnavailable, backedOff, leftForSteward, wokeStewards };
|
|
358
374
|
}
|
|
359
375
|
|
|
360
376
|
// Coalesced, fire-and-forget per-repo land trigger (see module header). A run already
|
package/src/workspace-merge.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { getLandingConfig } from "./config-store";
|
|
15
15
|
import type {
|
|
16
16
|
Command,
|
|
17
|
+
Orchestrator,
|
|
17
18
|
WorkspaceAutoMergePolicy,
|
|
18
19
|
WorkspaceLandingPolicy,
|
|
19
20
|
WorkspaceMergeStrategy,
|
|
@@ -142,6 +143,39 @@ export function withOwnerOnline<T extends { ownerAgentId?: string }>(workspace:
|
|
|
142
143
|
return { ...workspace, ownerOnline: isOwnerAlive(workspace.ownerAgentId) };
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
function workspaceOwnerHostKeys(workspace: WorkspaceRecord): Set<string> {
|
|
147
|
+
const keys = new Set<string>();
|
|
148
|
+
const owner = workspace.ownerAgentId ? getAgent(workspace.ownerAgentId) : undefined;
|
|
149
|
+
if (!owner) return keys;
|
|
150
|
+
if (owner.machine) keys.add(owner.machine);
|
|
151
|
+
const meta = isRecord(owner.meta) ? owner.meta : undefined;
|
|
152
|
+
const orchestratorId = meta ? stringValue(meta.orchestratorId) : undefined;
|
|
153
|
+
if (orchestratorId) keys.add(orchestratorId);
|
|
154
|
+
if (orchestratorId) keys.add(`orchestrator-${orchestratorId}`);
|
|
155
|
+
return keys;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Online hosts that can operate on a workspace, ordered by ownership first.
|
|
160
|
+
* The fallback to any matching host is intentional for manual recovery, but the
|
|
161
|
+
* owner host must win when known: isolated worktree paths are host-local.
|
|
162
|
+
*/
|
|
163
|
+
export function workspaceMergeOrchestrators(
|
|
164
|
+
workspace: WorkspaceRecord,
|
|
165
|
+
orchestrators: Orchestrator[] = listOrchestrators(),
|
|
166
|
+
): Orchestrator[] {
|
|
167
|
+
const candidates = orchestrators.filter((candidate) =>
|
|
168
|
+
candidate.status === "online" && isPathWithinBase(workspace.sourceCwd, candidate.baseDir),
|
|
169
|
+
);
|
|
170
|
+
const ownerKeys = workspaceOwnerHostKeys(workspace);
|
|
171
|
+
if (ownerKeys.size === 0) return candidates;
|
|
172
|
+
return [...candidates].sort((a, b) => {
|
|
173
|
+
const aOwner = ownerKeys.has(a.id) || ownerKeys.has(a.hostname) || ownerKeys.has(a.agentId) ? 0 : 1;
|
|
174
|
+
const bOwner = ownerKeys.has(b.id) || ownerKeys.has(b.hostname) || ownerKeys.has(b.agentId) ? 0 : 1;
|
|
175
|
+
return aOwner - bOwner;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
145
179
|
/**
|
|
146
180
|
* Resolved cwds of every live (non-offline) agent, including the workspace sourceCwd
|
|
147
181
|
* they were spawned in. The third "nobody is using it" signal the reaper needs: a
|
|
@@ -206,9 +240,7 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
|
|
|
206
240
|
}
|
|
207
241
|
try {
|
|
208
242
|
// Merge needs a live host: rebasing against a stale base later is unsafe.
|
|
209
|
-
const onlineOwner =
|
|
210
|
-
(candidate) => candidate.status === "online" && isPathWithinBase(workspace.sourceCwd, candidate.baseDir),
|
|
211
|
-
);
|
|
243
|
+
const onlineOwner = workspaceMergeOrchestrators(workspace)[0];
|
|
212
244
|
if (!onlineOwner) {
|
|
213
245
|
releaseMergeLease({ repoRoot: workspace.repoRoot, workspaceId: workspace.id });
|
|
214
246
|
return { ok: false, status: 409, error: "no online orchestrator available for workspace merge" };
|