agent-relay-server 0.119.1 → 0.119.3
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 +1 -1
- package/package.json +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +11 -0
- package/src/db/migrations.ts +6 -4
- package/src/db/schema.ts +6 -3
- package/src/issue-tracker/github.ts +45 -10
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.
|
|
5
|
+
"version": "0.119.3",
|
|
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.
|
|
3
|
+
"version": "0.119.3",
|
|
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.
|
|
40
|
+
"agent-relay-channels-host": "0.119.3",
|
|
41
41
|
"agent-relay-providers": "0.104.2",
|
|
42
|
-
"agent-relay-sdk": "0.2.
|
|
42
|
+
"agent-relay-sdk": "0.2.104",
|
|
43
43
|
"ajv": "^8.20.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
@@ -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
|
package/src/db/migrations.ts
CHANGED
|
@@ -201,7 +201,7 @@ export function applyMigrations(): void {
|
|
|
201
201
|
getDb().run("ALTER TABLE activity_events ADD COLUMN issue_action TEXT");
|
|
202
202
|
}
|
|
203
203
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_activity_issue ON activity_events(issue_ref_id, created_at)");
|
|
204
|
-
getDb().run("CREATE INDEX IF NOT EXISTS idx_activity_agent_issue ON activity_events(agent_id, issue_ref_id, created_at)");
|
|
204
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_activity_agent_issue ON activity_events(agent_id, issue_ref_id, created_at)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_activity_policy_created ON activity_events(json_extract(metadata, '$.policyName'), created_at DESC, id DESC)");
|
|
205
205
|
|
|
206
206
|
getDb().run(`
|
|
207
207
|
CREATE TABLE IF NOT EXISTS workspace_merge_pauses (
|
|
@@ -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).
|
|
@@ -351,7 +352,7 @@ export function applyMigrations(): void {
|
|
|
351
352
|
created_at INTEGER NOT NULL
|
|
352
353
|
)
|
|
353
354
|
`);
|
|
354
|
-
getDb().run("CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC)");
|
|
355
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_mda_agent_created ON message_delivery_attempts(agent_id, created_at DESC)");
|
|
355
356
|
|
|
356
357
|
const channelBindingsSql = getDb().query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'channel_bindings'").get() as { sql?: string } | undefined;
|
|
357
358
|
if (channelBindingsSql?.sql?.includes("UNIQUE(channel_id, conversation_key)")) {
|
|
@@ -397,7 +398,7 @@ export function applyMigrations(): void {
|
|
|
397
398
|
getDb().run("ALTER TABLE messages ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'");
|
|
398
399
|
}
|
|
399
400
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_kind ON messages(kind)");
|
|
400
|
-
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_kind_id ON messages(from_agent, kind, id)");
|
|
401
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_kind_id ON messages(from_agent, kind, id)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_analytics_created ON messages(created_at, kind, from_agent, claimable, reply_to)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_session_analytics ON messages(kind, created_at, json_extract(payload, '$.session.type'), json_extract(payload, '$.session.label'), json_extract(payload, '$.session.turnId'))");
|
|
401
402
|
// (from_agent, idempotency_key) powers findMessageByIdempotencyKey — the retry-dedup lookup
|
|
402
403
|
// AND the session-step in-place upsert (Codex tool running→completed / streamed reasoning),
|
|
403
404
|
// which runs per persisted session step. Added here (not the base schema) because the column
|
|
@@ -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");
|
|
@@ -1184,6 +1186,6 @@ export function applyMigrations(): void {
|
|
|
1184
1186
|
.query("SELECT 1 FROM sqlite_master WHERE type='index' AND name='idx_msg_to'")
|
|
1185
1187
|
.get();
|
|
1186
1188
|
getDb().run("DROP INDEX IF EXISTS idx_msg_to");
|
|
1187
|
-
getDb().run("DROP INDEX IF EXISTS idx_msg_resolved_to_agent");
|
|
1189
|
+
getDb().run("DROP INDEX IF EXISTS idx_msg_resolved_to_agent"); getDb().run("CREATE INDEX IF NOT EXISTS idx_commands_target_updated ON commands(target, updated_at DESC, created_at DESC)");
|
|
1188
1190
|
if (hasOldToIdx) getDb().run("ANALYZE messages");
|
|
1189
1191
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -509,7 +509,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
509
509
|
poison_reason TEXT,
|
|
510
510
|
created_at INTEGER NOT NULL
|
|
511
511
|
);
|
|
512
|
-
CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC);
|
|
512
|
+
CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_mda_agent_created ON message_delivery_attempts(agent_id, created_at DESC);
|
|
513
513
|
|
|
514
514
|
CREATE TABLE IF NOT EXISTS config (
|
|
515
515
|
namespace TEXT NOT NULL,
|
|
@@ -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
|
|
@@ -861,7 +864,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
861
864
|
);
|
|
862
865
|
CREATE INDEX IF NOT EXISTS idx_activity_operator ON activity_events(operator_id, created_at);
|
|
863
866
|
CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_events(created_at);
|
|
864
|
-
CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity_events(agent_id, created_at);
|
|
867
|
+
CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity_events(agent_id, created_at); CREATE INDEX IF NOT EXISTS idx_activity_policy_created ON activity_events(json_extract(metadata, '$.policyName'), created_at DESC, id DESC);
|
|
865
868
|
-- issue_ref_id/issue_action indexes are created in applyMigrations() AFTER the ALTER — never inline here (#483: inline index over a not-yet-migrated column crashes initDb on existing DBs).
|
|
866
869
|
|
|
867
870
|
CREATE TABLE IF NOT EXISTS channels (
|
|
@@ -935,7 +938,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
935
938
|
updated_at INTEGER NOT NULL,
|
|
936
939
|
expires_at INTEGER
|
|
937
940
|
);
|
|
938
|
-
CREATE INDEX IF NOT EXISTS idx_commands_target ON commands(target);
|
|
941
|
+
CREATE INDEX IF NOT EXISTS idx_commands_target ON commands(target); CREATE INDEX IF NOT EXISTS idx_commands_target_updated ON commands(target, updated_at DESC, created_at DESC);
|
|
939
942
|
CREATE INDEX IF NOT EXISTS idx_commands_status ON commands(status);
|
|
940
943
|
CREATE INDEX IF NOT EXISTS idx_commands_created ON commands(created_at);
|
|
941
944
|
CREATE INDEX IF NOT EXISTS idx_commands_correlation ON commands(correlation_id);
|
|
@@ -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
|
-
// #
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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)) {
|