agent-relay-server 0.118.3 → 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.3",
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",
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.3",
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"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.118.3",
4
+ "version": "0.118.4",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -21,7 +21,6 @@
21
21
  import {
22
22
  createActivityEvent,
23
23
  getActiveMergePause,
24
- getAgent,
25
24
  listOrchestrators,
26
25
  listWorkspaces,
27
26
  patchWorkspaceMetadata,
@@ -36,10 +35,10 @@ import { renderTemplate } from "./prompt-resolver";
36
35
  import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
37
36
  import { emitWorkspaceTaskSemanticNotifications } from "./services/task-lifecycle";
38
37
  import { ensureRepoSteward } from "./steward";
39
- import { requestWorkspaceMerge } from "./workspace-merge";
38
+ import { requestWorkspaceMerge, workspaceMergeOrchestrators } from "./workspace-merge";
40
39
  import { workspaceActiveClaim } from "./workspace-claim";
41
40
  import { DIRTY_WORKTREE_LAND_SKIP_REASON, READY_TO_LAND_STATUSES } from "./workspace-phase";
42
- import { fetchHostMergePreview, workspacePathWithinBase } from "./workspace-probe";
41
+ import { fetchHostMergePreview } from "./workspace-probe";
43
42
  import type { WorkspaceAutoMergePolicy, WorkspaceLandingPolicy, WorkspaceMergePreview, WorkspaceMergeStrategy, WorkspaceRecord } from "./types";
44
43
  import { STEWARD_WAKE_COOLDOWN_MS, stewardFallbackTarget, workspaceAutoMergeEnabled } from "./config";
45
44
 
@@ -263,25 +262,11 @@ function emptyLandSweep(): LandSweepResult {
263
262
  return { scanned: 0, merged: [], heldByLease: [], heldByClaim: [], heldByPause: [], heldAwaitingApproval: [], previewUnavailable: [], backedOff: [], leftForSteward: [], wokeStewards: [] };
264
263
  }
265
264
 
266
- function workspaceOrchestrators(
267
- ws: WorkspaceRecord,
268
- orchestrators: ReturnType<typeof listOrchestrators>,
269
- ): ReturnType<typeof listOrchestrators> {
270
- const candidates = orchestrators.filter((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
271
- const ownerMachine = ws.ownerAgentId ? getAgent(ws.ownerAgentId)?.machine : undefined;
272
- if (!ownerMachine) return candidates;
273
- return [...candidates].sort((a, b) => {
274
- const aOwner = a.hostname === ownerMachine || a.id === ownerMachine ? 0 : 1;
275
- const bOwner = b.hostname === ownerMachine || b.id === ownerMachine ? 0 : 1;
276
- return aOwner - bOwner;
277
- });
278
- }
279
-
280
265
  async function fetchFirstUsableMergePreview(
281
266
  ws: WorkspaceRecord,
282
267
  orchestrators: ReturnType<typeof listOrchestrators>,
283
268
  ): Promise<WorkspaceMergePreview | { available: false } | null> {
284
- for (const orch of workspaceOrchestrators(ws, orchestrators)) {
269
+ for (const orch of workspaceMergeOrchestrators(ws, orchestrators)) {
285
270
  if (!orch.apiUrl) continue;
286
271
  const preview = await fetchHostMergePreview(orch.apiUrl, ws);
287
272
  if (!preview || (preview as { available?: false }).available === false) continue;
@@ -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 = listOrchestrators().find(
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" };