agent-relay-server 0.101.0 → 0.101.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Relay API",
5
- "version": "0.101.0",
5
+ "version": "0.101.1",
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.101.0",
3
+ "version": "0.101.1",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "CONTRIBUTING.md"
35
35
  ],
36
36
  "dependencies": {
37
- "agent-relay-sdk": "0.2.82",
37
+ "agent-relay-sdk": "0.2.83",
38
38
  "ajv": "^8.20.0"
39
39
  },
40
40
  "scripts": {
@@ -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.101.0",
4
+ "version": "0.101.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -20,7 +20,7 @@
20
20
  import { emitRelayEvent } from "./events";
21
21
  import { routeNotification } from "./notification-router";
22
22
  import { getSpawnCapableAgentIds } from "./routing-policy-push";
23
- import { getProviderBandInfo } from "./spawn-targets";
23
+ import { getProviderBandInfo, windowRecommendedAction } from "./spawn-targets";
24
24
  import type { ProviderBandInfo, ProviderBandWindow, QuotaBand } from "./spawn-targets";
25
25
  import type { ProviderQuotaAdvisoryInput } from "./db/provider-quotas";
26
26
 
@@ -112,7 +112,10 @@ export function processBandTransitions(input: ProviderQuotaAdvisoryInput): void
112
112
  provider: input.provider,
113
113
  window,
114
114
  previousBand,
115
- recommendedAction: info.recommendedAction,
115
+ // #761 — use the window's own band for the action text, not the provider-level band.
116
+ // e.g. a 5h→prefer transition on a provider whose 7d is at avoid-large must say
117
+ // "Prefer…", not "Avoid large jobs…".
118
+ recommendedAction: windowRecommendedAction(effectiveBand, window.slack),
116
119
  now: input.now,
117
120
  });
118
121
  } else {
@@ -362,6 +362,21 @@ export function auditEvent(input: ActivityEventInput): void {
362
362
  }
363
363
  }
364
364
 
365
+ /**
366
+ * Derive a human-readable actor label from the request's auth context, for use as
367
+ * `updatedBy` in config writes. Prefixes distinguish the auth type so notifications
368
+ * show "by dashboard:alice" rather than an opaque id.
369
+ */
370
+ export function resolveActorLabel(req: Request): string | undefined {
371
+ const acting = resolveActingUser(req);
372
+ if (acting) return `dashboard:${acting.userId}`;
373
+ const component = getComponentAuth(req);
374
+ if (component) return `agent:${component.sub}`;
375
+ const integration = getIntegrationAuth(req);
376
+ if (integration) return `integration:${integration.name}`;
377
+ return undefined;
378
+ }
379
+
365
380
  export function memoryContext(req: Request): MemoryBrokerContext {
366
381
  const component = getComponentAuth(req);
367
382
  if (component) {
@@ -2,7 +2,7 @@
2
2
  // GET: agent:read (coordinators read the policy to apply rules).
3
3
  // PUT: settings:write:routing (admin writes).
4
4
  import { ValidationError } from "../db";
5
- import { error, json, parseBody, type Handler } from "./_shared";
5
+ import { error, json, parseBody, resolveActorLabel, type Handler } from "./_shared";
6
6
  import { getRoutingPolicy, setRoutingPolicy } from "../routing-policy-store";
7
7
  import { isRecord } from "agent-relay-sdk";
8
8
  import { cleanString } from "../validation";
@@ -15,7 +15,8 @@ export const putRoutingPolicyRoute: Handler = async (req) => {
15
15
  const parsed = await parseBody<unknown>(req);
16
16
  if (!parsed.ok) return error(parsed.error, parsed.status);
17
17
  try {
18
- const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
18
+ const bodyUpdatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
19
+ const updatedBy = bodyUpdatedBy ?? resolveActorLabel(req);
19
20
  const policy = setRoutingPolicy(parsed.body, { updatedBy });
20
21
  return json(policy);
21
22
  } catch (e) {
@@ -31,9 +31,17 @@ export function getRoutingPolicy(): RoutingPolicy {
31
31
  * Persist the routing policy. The incoming value is normalized so partial bodies
32
32
  * still produce a valid document. Emits `config.changed` via `setConfig` so the
33
33
  * dashboard + any live listeners pick it up immediately.
34
+ *
35
+ * No-op detection: if the normalized incoming policy is deep-equal to the current
36
+ * persisted one, the write and the fleet notification are both skipped so identical
37
+ * saves are fully silent.
34
38
  */
35
39
  export function setRoutingPolicy(value: unknown, opts: { updatedBy?: string } = {}): RoutingPolicy {
36
40
  const normalized = normalizeRoutingPolicy(value);
41
+ const current = getRoutingPolicy();
42
+ if (JSON.stringify(normalized) === JSON.stringify(current)) {
43
+ return normalized;
44
+ }
37
45
  setConfig(ROUTING_POLICY_NAMESPACE, ROUTING_POLICY_KEY, normalized, opts.updatedBy);
38
46
  notifyRoutingPolicyChanged(normalized, opts);
39
47
  return normalized;
@@ -356,12 +356,20 @@ function recommendedAction(input: { quotaPenaltyBand: SpawnQuotaBurnBand; blocke
356
356
  ? "Quota is blocked now; switch provider or escalate unless this provider was explicitly requested."
357
357
  : "5h quota is blocked now; wait for reset or use another provider unless the job is urgent.";
358
358
  }
359
- if (input.quotaPenaltyBand === "hard-avoid") return "Hard avoid unless explicitly requested or no healthier alternate exists.";
360
- if (input.quotaPenaltyBand === "avoid-large") return "Avoid large jobs if a healthier alternate exists.";
361
- if (input.quotaPenaltyBand === "caution") return "Caution: prefer cheaper effort/model when practical.";
359
+ return windowRecommendedAction(input.quotaPenaltyBand, input.slack);
360
+ }
361
+
362
+ /**
363
+ * Per-window recommended action text for a given band — used by band-transition notifications
364
+ * so each window's notification reflects ITS own band, not the provider-level band.
365
+ */
366
+ export function windowRecommendedAction(band: QuotaBand, slack: number | null): string {
367
+ if (band === "hard-avoid") return "Hard avoid unless explicitly requested or no healthier alternate exists.";
368
+ if (band === "avoid-large") return "Avoid large jobs if a healthier alternate exists.";
369
+ if (band === "caution") return "Caution: prefer cheaper effort/model when practical.";
362
370
  // #760 — surface the underburn signal so coordinators load-shift big/deferrable jobs here.
363
- if (input.quotaPenaltyBand === "prefer") {
364
- const headroom = input.slack !== null ? ` (~${Math.round(input.slack * 100)}% quota would otherwise go unused at reset)` : "";
371
+ if (band === "prefer") {
372
+ const headroom = slack !== null ? ` (~${Math.round(slack * 100)}% quota would otherwise go unused at reset)` : "";
365
373
  return `Prefer for large or deferrable jobs${headroom}.`;
366
374
  }
367
375
  return "Spawn normally.";