agent-relay-server 0.119.1 → 0.119.2

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.119.1",
5
+ "version": "0.119.2",
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.119.1",
3
+ "version": "0.119.2",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -37,9 +37,9 @@
37
37
  "CONTRIBUTING.md"
38
38
  ],
39
39
  "dependencies": {
40
- "agent-relay-channels-host": "0.119.1",
40
+ "agent-relay-channels-host": "0.119.2",
41
41
  "agent-relay-providers": "0.104.2",
42
- "agent-relay-sdk": "0.2.103",
42
+ "agent-relay-sdk": "0.2.104",
43
43
  "ajv": "^8.20.0"
44
44
  },
45
45
  "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.119.1",
4
+ "version": "0.119.2",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -344,6 +344,10 @@ function relayTokenFromEnv() {
344
344
  }
345
345
  // sdk/src/bus-client.ts
346
346
  import { EventEmitter } from "events";
347
+ function unrefTimer(timer) {
348
+ timer?.unref?.();
349
+ }
350
+
347
351
  class MemoryCursorStore {
348
352
  value = null;
349
353
  async load() {
@@ -655,6 +659,7 @@ class RelayBusClient extends EventEmitter {
655
659
  this.heartbeat = setInterval(() => {
656
660
  this.sendHeartbeat();
657
661
  }, this.options.heartbeatIntervalMs ?? 30000);
662
+ unrefTimer(this.heartbeat);
658
663
  }
659
664
  stopHeartbeat() {
660
665
  if (this.heartbeat)
@@ -667,6 +672,7 @@ class RelayBusClient extends EventEmitter {
667
672
  this.stopHeartbeatWatchdog();
668
673
  const intervalMs = Math.max(100, Math.min(5000, this.heartbeatIntervalMs()));
669
674
  this.heartbeatWatchdog = setInterval(() => this.checkHeartbeatAck(), intervalMs);
675
+ unrefTimer(this.heartbeatWatchdog);
670
676
  }
671
677
  stopHeartbeatWatchdog() {
672
678
  if (this.heartbeatWatchdog)
@@ -753,6 +759,10 @@ class RelayBusClient extends EventEmitter {
753
759
  }
754
760
  }
755
761
  // sdk/src/provider-base.ts
762
+ function unrefTimer2(timer) {
763
+ timer?.unref?.();
764
+ }
765
+
756
766
  class ProviderBase {
757
767
  options;
758
768
  client;
@@ -814,6 +824,7 @@ class ProviderBase {
814
824
  this.client.status({ agentStatus: "online", ready: false });
815
825
  }
816
826
  }, 30000);
827
+ unrefTimer2(this.drain);
817
828
  }
818
829
  }
819
830
  // sdk/src/claim-tracker.ts
@@ -276,6 +276,7 @@ export function applyMigrations(): void {
276
276
  getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_thread ON messages(thread_id)");
277
277
  getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key) WHERE idempotency_key IS NOT NULL");
278
278
  getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_delivery_status ON messages(delivery_status)");
279
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_claimed_expires ON messages(claimed_by, claim_expires_at, id) WHERE claimed_by IS NOT NULL");
279
280
  // (#846) composite replaces the old single-column idx_msg_resolved_to_agent; created
280
281
  // here — AFTER the resolved_to_agent ALTER above — so existing DBs without the column
281
282
  // don't crash (same hazard as idx_msg_from_kind_id / #483/#559).
@@ -1076,6 +1077,7 @@ export function applyMigrations(): void {
1076
1077
  addTaskCol("owner_agent_label", "owner_agent_label TEXT");
1077
1078
  addTaskCol("dispatch", "dispatch TEXT");
1078
1079
  addTaskCol("reaped_at", "reaped_at INTEGER");
1080
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_message_status ON tasks(message_id, status) WHERE message_id IS NOT NULL");
1079
1081
  getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_kind ON tasks(kind, status, updated_at)");
1080
1082
  getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_branch ON tasks(branch) WHERE branch IS NOT NULL");
1081
1083
  getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id) WHERE team_id IS NOT NULL");
package/src/db/schema.ts CHANGED
@@ -672,6 +672,9 @@ export function initDb(path: string = "agent-relay.db"): Database {
672
672
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
673
673
  CREATE INDEX IF NOT EXISTS idx_tasks_target ON tasks(target);
674
674
  CREATE INDEX IF NOT EXISTS idx_tasks_updated ON tasks(updated_at);
675
+ CREATE INDEX IF NOT EXISTS idx_tasks_message_status
676
+ ON tasks(message_id, status)
677
+ WHERE message_id IS NOT NULL;
675
678
  -- idx_tasks_kind / idx_tasks_branch / idx_tasks_team reference the #834 columns and are
676
679
  -- created in applyMigrations() AFTER the ALTERs — never inline here: an existing pre-#834
677
680
  -- tasks table makes CREATE TABLE IF NOT EXISTS a no-op, so an inline index over a column
@@ -3,8 +3,9 @@ import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerIssueDe
3
3
 
4
4
  export const GITHUB_FIXED_LABEL = "fixed";
5
5
  const GITHUB_CREDENTIAL_REQUIREMENT = "GitHub issue tracker adapter requires gh authenticated with a token that has issues:write scope";
6
- // #838hard bound on the spawn-time issue read so hydration never stalls the spawn.
7
- const ISSUE_DETAIL_READ_TIMEOUT_MS = 10_000;
6
+ // #935 — spawn-time issue hydration sits on the dashboard /agents/spawn -> 202 path.
7
+ // Keep it sub-second and degrade to "spec unavailable" if GitHub is slow.
8
+ const ISSUE_DETAIL_READ_TIMEOUT_MS = 750;
8
9
 
9
10
  type GitHubApiMethod = "GET" | "PATCH" | "POST";
10
11
 
@@ -83,19 +84,19 @@ async function ghApi(request: GitHubApiRequest): Promise<unknown> {
83
84
  // auth backed by a GitHub token with `issues:write` scope.
84
85
  const args = ["gh", "api", "--method", request.method, request.path, "--header", "Accept: application/vnd.github+json"];
85
86
  appendBodyArgs(args, request.body);
86
- // `Bun.spawnSync` blocks the event loop synchronously, so a JS-level timeout race can't preempt
87
- // a hung `gh`; the subprocess `timeout` (kills with SIGTERM on expiry) is the only effective bound.
88
- const proc = Bun.spawnSync(args, {
87
+ const proc = Bun.spawn(args, {
89
88
  stdin: "ignore",
90
89
  stdout: "pipe",
91
90
  stderr: "pipe",
92
91
  env: process.env,
93
- ...(request.timeoutMs ? { timeout: request.timeoutMs } : {}),
94
92
  });
95
- const stdout = textFromBytes(proc.stdout).trim();
96
- if (proc.exitCode !== 0) {
97
- const stderr = textFromBytes(proc.stderr).trim();
98
- const timedOut = request.timeoutMs && (proc.signalCode === "SIGTERM" || proc.exitCode === null);
93
+ const stdoutPromise = streamText(proc.stdout);
94
+ const stderrPromise = streamText(proc.stderr);
95
+ const timedOut = await waitForExitOrTimeout(proc, request.timeoutMs);
96
+ const [stdoutRaw, stderrRaw] = await Promise.all([stdoutPromise, stderrPromise]);
97
+ const stdout = stdoutRaw.trim();
98
+ if (timedOut || proc.exitCode !== 0) {
99
+ const stderr = stderrRaw.trim();
99
100
  throw new Error(`${GITHUB_CREDENTIAL_REQUIREMENT}; gh api ${timedOut ? `timed out after ${request.timeoutMs}ms` : "failed"}: ${stderr || stdout || `exit ${proc.exitCode}`}`);
100
101
  }
101
102
  if (!stdout) return {};
@@ -106,6 +107,40 @@ async function ghApi(request: GitHubApiRequest): Promise<unknown> {
106
107
  }
107
108
  }
108
109
 
110
+ async function streamText(stream: ReadableStream<Uint8Array> | null): Promise<string> {
111
+ if (!stream) return "";
112
+ try {
113
+ return await new Response(stream).text();
114
+ } catch {
115
+ return "";
116
+ }
117
+ }
118
+
119
+ async function waitForExitOrTimeout(proc: Bun.Subprocess<"ignore", "pipe", "pipe">, timeoutMs: number | undefined): Promise<boolean> {
120
+ if (!timeoutMs || timeoutMs <= 0) {
121
+ await proc.exited;
122
+ return false;
123
+ }
124
+ let timedOut = false;
125
+ let kill: ReturnType<typeof setTimeout> | undefined;
126
+ const timeout = setTimeout(() => {
127
+ timedOut = true;
128
+ try { proc.kill("SIGTERM"); } catch {}
129
+ kill = setTimeout(() => {
130
+ try { proc.kill("SIGKILL"); } catch {}
131
+ }, 1_000);
132
+ kill.unref?.();
133
+ }, timeoutMs);
134
+ timeout.unref?.();
135
+ try {
136
+ await proc.exited;
137
+ } finally {
138
+ clearTimeout(timeout);
139
+ if (kill) clearTimeout(kill);
140
+ }
141
+ return timedOut;
142
+ }
143
+
109
144
  function appendBodyArgs(args: string[], body: Record<string, unknown> | undefined): void {
110
145
  if (!body) return;
111
146
  for (const [key, value] of Object.entries(body)) {