agent-relay 3.2.22 → 4.0.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/README.md +5 -5
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +6564 -2100
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +14 -4
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts +2 -6
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +31 -12
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +10 -3
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/commands/monitoring.d.ts +2 -2
- package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/src/cli/commands/monitoring.js +15 -6
- package/dist/src/cli/commands/monitoring.js.map +1 -1
- package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
- package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
- package/dist/src/cli/commands/on/dotfiles.js +157 -0
- package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
- package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
- package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
- package/dist/src/cli/commands/on/prereqs.js +103 -0
- package/dist/src/cli/commands/on/prereqs.js.map +1 -0
- package/dist/src/cli/commands/on/provision.d.ts +22 -0
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
- package/dist/src/cli/commands/on/provision.js +157 -0
- package/dist/src/cli/commands/on/provision.js.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
- package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
- package/dist/src/cli/commands/on/scan.d.ts +8 -0
- package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
- package/dist/src/cli/commands/on/scan.js +59 -0
- package/dist/src/cli/commands/on/scan.js.map +1 -0
- package/dist/src/cli/commands/on/services.d.ts +17 -0
- package/dist/src/cli/commands/on/services.d.ts.map +1 -0
- package/dist/src/cli/commands/on/services.js +328 -0
- package/dist/src/cli/commands/on/services.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts +61 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -0
- package/dist/src/cli/commands/on/start.js +1107 -0
- package/dist/src/cli/commands/on/start.js.map +1 -0
- package/dist/src/cli/commands/on/stop.d.ts +4 -0
- package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
- package/dist/src/cli/commands/on/stop.js +11 -0
- package/dist/src/cli/commands/on/stop.js.map +1 -0
- package/dist/src/cli/commands/on/token.d.ts +8 -0
- package/dist/src/cli/commands/on/token.d.ts.map +1 -0
- package/dist/src/cli/commands/on/token.js +26 -0
- package/dist/src/cli/commands/on/token.js.map +1 -0
- package/dist/src/cli/commands/on/workspace.d.ts +4 -0
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
- package/dist/src/cli/commands/on/workspace.js +245 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts +10 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -0
- package/dist/src/cli/commands/on.js +52 -0
- package/dist/src/cli/commands/on.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +10 -21
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/bridge.js +1 -1
- package/dist/src/cli/lib/bridge.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +82 -120
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +4 -4
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js +14 -11
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
- package/dist/src/cli/lib/core-maintenance.js +11 -22
- package/dist/src/cli/lib/core-maintenance.js.map +1 -1
- package/dist/src/cost/pricing.d.ts +18 -0
- package/dist/src/cost/pricing.d.ts.map +1 -0
- package/dist/src/cost/pricing.js +111 -0
- package/dist/src/cost/pricing.js.map +1 -0
- package/dist/src/cost/tracker.d.ts +13 -0
- package/dist/src/cost/tracker.d.ts.map +1 -0
- package/dist/src/cost/tracker.js +152 -0
- package/dist/src/cost/tracker.js.map +1 -0
- package/dist/src/cost/types.d.ts +23 -0
- package/dist/src/cost/types.d.ts.map +1 -0
- package/dist/src/cost/types.js +2 -0
- package/dist/src/cost/types.js.map +1 -0
- package/package.json +15 -12
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +3 -3
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +10 -3
- package/packages/sdk/dist/broker-path.d.ts +3 -2
- package/packages/sdk/dist/broker-path.d.ts.map +1 -1
- package/packages/sdk/dist/broker-path.js +119 -32
- package/packages/sdk/dist/broker-path.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +119 -197
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +354 -823
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +2 -5
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.d.ts +9 -26
- package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +75 -47
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -6
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +213 -43
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/transport.d.ts +58 -0
- package/packages/sdk/dist/transport.d.ts.map +1 -0
- package/packages/sdk/dist/transport.js +184 -0
- package/packages/sdk/dist/transport.js.map +1 -0
- package/packages/sdk/dist/types.d.ts +69 -0
- package/packages/sdk/dist/types.d.ts.map +1 -0
- package/packages/sdk/dist/types.js +5 -0
- package/packages/sdk/dist/types.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +3 -2
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +1 -3
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
- package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
- package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +7 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +7 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
- package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/process-spawner.js +141 -0
- package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
- package/packages/sdk/dist/workflows/run.d.ts +2 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +6 -6
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +443 -719
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
- package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/step-executor.js +393 -0
- package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.js +144 -0
- package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +17 -2
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/dist/workflows/verification.d.ts +33 -0
- package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/verification.js +122 -0
- package/packages/sdk/dist/workflows/verification.js.map +1 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +100 -1
- package/packages/sdk/src/broker-path.ts +136 -30
- package/packages/sdk/src/client.ts +453 -1069
- package/packages/sdk/src/examples/example.ts +2 -5
- package/packages/sdk/src/index.ts +9 -1
- package/packages/sdk/src/relay-adapter.ts +75 -55
- package/packages/sdk/src/relay.ts +262 -55
- package/packages/sdk/src/transport.ts +216 -0
- package/packages/sdk/src/types.ts +75 -0
- package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
- package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
- package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
- package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
- package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
- package/packages/sdk/src/workflows/builder.ts +6 -6
- package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
- package/packages/sdk/src/workflows/index.ts +12 -0
- package/packages/sdk/src/workflows/process-spawner.ts +201 -0
- package/packages/sdk/src/workflows/run.ts +2 -1
- package/packages/sdk/src/workflows/runner.ts +636 -951
- package/packages/sdk/src/workflows/step-executor.ts +579 -0
- package/packages/sdk/src/workflows/template-resolver.ts +180 -0
- package/packages/sdk/src/workflows/validator.ts +20 -2
- package/packages/sdk/src/workflows/verification.ts +184 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
- package/packages/sdk-py/src/agent_relay/client.py +329 -522
- package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
- package/packages/sdk-py/src/agent_relay/relay.py +1 -4
- package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
- package/packages/sdk-py/uv.lock +5388 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -1
- package/packages/telemetry/dist/client.js +1 -1
- package/packages/telemetry/dist/client.js.map +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +3 -10
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/scripts/postinstall.js +121 -1
|
@@ -24,15 +24,18 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { randomBytes } from 'node:crypto';
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
28
|
import path from 'node:path';
|
|
28
29
|
|
|
30
|
+
import { RelayCast } from '@relaycast/sdk';
|
|
31
|
+
|
|
29
32
|
import {
|
|
30
33
|
AgentRelayClient,
|
|
31
|
-
|
|
32
|
-
type
|
|
33
|
-
type SendMessageInput,
|
|
34
|
-
type SpawnPtyInput,
|
|
34
|
+
type AgentRelayBrokerInitArgs,
|
|
35
|
+
type AgentRelaySpawnOptions,
|
|
35
36
|
} from './client.js';
|
|
37
|
+
import { AgentRelayProtocolError } from './transport.js';
|
|
38
|
+
import type { SendMessageInput, SpawnPtyInput } from './types.js';
|
|
36
39
|
import type {
|
|
37
40
|
AgentRuntime,
|
|
38
41
|
BrokerEvent,
|
|
@@ -75,6 +78,71 @@ function buildUnsupportedOperationMessage(
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
interface WorkspaceRegistryEntry {
|
|
82
|
+
relaycastApiKey?: string;
|
|
83
|
+
relayfileUrl?: string;
|
|
84
|
+
createdAt?: string;
|
|
85
|
+
agents?: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type WorkspaceRegistry = Record<string, WorkspaceRegistryEntry>;
|
|
89
|
+
|
|
90
|
+
const WORKSPACE_ID_PREFIX = 'rw_';
|
|
91
|
+
const WORKSPACE_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
92
|
+
|
|
93
|
+
function normalizeWorkspaceId(value: string | undefined): string | undefined {
|
|
94
|
+
const trimmed = value?.trim();
|
|
95
|
+
return trimmed ? trimmed : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function generateWorkspaceId(): string {
|
|
99
|
+
const alphabetLength = WORKSPACE_ID_ALPHABET.length;
|
|
100
|
+
const maxUnbiasedValue = Math.floor(256 / alphabetLength) * alphabetLength;
|
|
101
|
+
let suffix = '';
|
|
102
|
+
|
|
103
|
+
while (suffix.length < 8) {
|
|
104
|
+
const bytes = randomBytes(8 - suffix.length);
|
|
105
|
+
for (const byte of bytes) {
|
|
106
|
+
if (byte >= maxUnbiasedValue) continue;
|
|
107
|
+
suffix += WORKSPACE_ID_ALPHABET[byte % alphabetLength];
|
|
108
|
+
if (suffix.length === 8) break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `${WORKSPACE_ID_PREFIX}${suffix}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toWorkspaceRegistryEntry(value: unknown): WorkspaceRegistryEntry {
|
|
116
|
+
if (!value || typeof value !== 'object') {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const record = value as Record<string, unknown>;
|
|
121
|
+
const relaycastApiKey =
|
|
122
|
+
typeof record.relaycastApiKey === 'string' && record.relaycastApiKey.trim()
|
|
123
|
+
? record.relaycastApiKey.trim()
|
|
124
|
+
: undefined;
|
|
125
|
+
const relayfileUrl =
|
|
126
|
+
typeof record.relayfileUrl === 'string' && record.relayfileUrl.trim()
|
|
127
|
+
? record.relayfileUrl.trim()
|
|
128
|
+
: undefined;
|
|
129
|
+
const createdAt =
|
|
130
|
+
typeof record.createdAt === 'string' && record.createdAt.trim() ? record.createdAt.trim() : undefined;
|
|
131
|
+
const agents = Array.isArray(record.agents)
|
|
132
|
+
? record.agents
|
|
133
|
+
.filter((agent): agent is string => typeof agent === 'string')
|
|
134
|
+
.map((agent) => agent.trim())
|
|
135
|
+
.filter((agent) => agent.length > 0)
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...(relaycastApiKey ? { relaycastApiKey } : {}),
|
|
140
|
+
...(relayfileUrl ? { relayfileUrl } : {}),
|
|
141
|
+
...(createdAt ? { createdAt } : {}),
|
|
142
|
+
...(agents && agents.length > 0 ? { agents } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
78
146
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
79
147
|
|
|
80
148
|
export interface Message {
|
|
@@ -201,7 +269,10 @@ export interface Agent {
|
|
|
201
269
|
* @param options.stream — if provided, only invoke callback when the event stream matches (e.g. 'stdout', 'stderr')
|
|
202
270
|
* @param options.mode — 'chunk' for raw string callbacks, 'structured' for { stream, chunk } callbacks. Auto-detected if omitted.
|
|
203
271
|
*/
|
|
204
|
-
onOutput(
|
|
272
|
+
onOutput(
|
|
273
|
+
callback: AgentOutputCallback,
|
|
274
|
+
options?: { stream?: string; mode?: 'chunk' | 'structured' }
|
|
275
|
+
): () => void;
|
|
205
276
|
}
|
|
206
277
|
|
|
207
278
|
export interface HumanHandle {
|
|
@@ -236,17 +307,24 @@ export type EventHook<T> = ((value: T) => void) | null;
|
|
|
236
307
|
|
|
237
308
|
export interface AgentRelayOptions {
|
|
238
309
|
binaryPath?: string;
|
|
239
|
-
binaryArgs?:
|
|
310
|
+
binaryArgs?: AgentRelayBrokerInitArgs;
|
|
240
311
|
brokerName?: string;
|
|
241
312
|
channels?: string[];
|
|
242
313
|
cwd?: string;
|
|
243
314
|
env?: NodeJS.ProcessEnv;
|
|
244
315
|
requestTimeoutMs?: number;
|
|
245
|
-
shutdownTimeoutMs?: number;
|
|
246
316
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
317
|
+
* Unified workspace ID shared across relayfile, relayauth claims, and
|
|
318
|
+
* relaycast key lookup.
|
|
319
|
+
*/
|
|
320
|
+
workspaceId?: string;
|
|
321
|
+
/**
|
|
322
|
+
* Display name for an auto-created Relaycast workspace.
|
|
323
|
+
* If omitted, the unified workspace ID is used.
|
|
324
|
+
*
|
|
325
|
+
* @deprecated Since v1.x this field falls back to workspaceId when omitted,
|
|
326
|
+
* changing prior behavior where it was required for workspace naming.
|
|
327
|
+
* Callers relying on distinct naming should set this explicitly.
|
|
250
328
|
*/
|
|
251
329
|
workspaceName?: string;
|
|
252
330
|
/**
|
|
@@ -302,14 +380,17 @@ export class AgentRelay {
|
|
|
302
380
|
readonly gemini: AgentSpawner;
|
|
303
381
|
readonly opencode: AgentSpawner;
|
|
304
382
|
|
|
305
|
-
private readonly clientOptions:
|
|
383
|
+
private readonly clientOptions: AgentRelaySpawnOptions;
|
|
306
384
|
private readonly defaultChannels: string[];
|
|
385
|
+
private readonly requestedWorkspaceId?: string;
|
|
307
386
|
private readonly workspaceName?: string;
|
|
308
387
|
private readonly relaycastBaseUrl?: string;
|
|
309
388
|
private relayApiKey?: string;
|
|
389
|
+
private resolvedWorkspaceId?: string;
|
|
310
390
|
private client?: AgentRelayClient;
|
|
311
391
|
private startPromise?: Promise<AgentRelayClient>;
|
|
312
392
|
private unsubEvent?: () => void;
|
|
393
|
+
private readonly stderrListeners = new Set<(line: string) => void>();
|
|
313
394
|
private readonly knownAgents = new Map<string, Agent>();
|
|
314
395
|
private readonly readyAgents = new Set<string>();
|
|
315
396
|
private readonly messageReadyAgents = new Set<string>();
|
|
@@ -329,18 +410,25 @@ export class AgentRelay {
|
|
|
329
410
|
private idleResolverSeq = 0;
|
|
330
411
|
|
|
331
412
|
constructor(options: AgentRelayOptions = {}) {
|
|
413
|
+
const requestedWorkspaceId = normalizeWorkspaceId(options.workspaceId);
|
|
332
414
|
this.defaultChannels = options.channels ?? ['general'];
|
|
415
|
+
this.requestedWorkspaceId = requestedWorkspaceId;
|
|
333
416
|
this.workspaceName = options.workspaceName;
|
|
417
|
+
if (options.workspaceName && !options.workspaceId) {
|
|
418
|
+
console.warn(
|
|
419
|
+
'[AgentRelay] workspaceName without workspaceId is deprecated and will be removed in a future major version. ' +
|
|
420
|
+
'Set workspaceId explicitly to avoid silent behavior changes.'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
334
423
|
this.relaycastBaseUrl = options.relaycastBaseUrl;
|
|
335
424
|
this.clientOptions = {
|
|
336
425
|
binaryPath: options.binaryPath,
|
|
337
426
|
binaryArgs: options.binaryArgs,
|
|
338
|
-
brokerName: options.brokerName ?? options.workspaceName,
|
|
427
|
+
brokerName: options.brokerName ?? options.workspaceName ?? requestedWorkspaceId,
|
|
339
428
|
channels: this.defaultChannels,
|
|
340
429
|
cwd: options.cwd,
|
|
341
430
|
env: options.env,
|
|
342
431
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
343
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
344
432
|
};
|
|
345
433
|
|
|
346
434
|
this.codex = this.createSpawner('codex', 'Codex', 'pty');
|
|
@@ -349,28 +437,122 @@ export class AgentRelay {
|
|
|
349
437
|
this.opencode = this.createSpawner('opencode', 'OpenCode', 'headless');
|
|
350
438
|
}
|
|
351
439
|
|
|
440
|
+
private getWorkspaceRegistryPath(): string {
|
|
441
|
+
return path.join(this.clientOptions.cwd ?? process.cwd(), '.relay', 'workspaces.json');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private readWorkspaceRegistry(): WorkspaceRegistry {
|
|
445
|
+
const registryPath = this.getWorkspaceRegistryPath();
|
|
446
|
+
if (!existsSync(registryPath)) {
|
|
447
|
+
return {};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let raw: string;
|
|
451
|
+
try {
|
|
452
|
+
raw = readFileSync(registryPath, 'utf8').trim();
|
|
453
|
+
} catch {
|
|
454
|
+
return {};
|
|
455
|
+
}
|
|
456
|
+
if (!raw) {
|
|
457
|
+
return {};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let parsed: unknown;
|
|
461
|
+
try {
|
|
462
|
+
parsed = JSON.parse(raw);
|
|
463
|
+
} catch {
|
|
464
|
+
// Registry file is corrupted (partial write, disk full, concurrent access).
|
|
465
|
+
// Return empty registry so callers can re-create it.
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
469
|
+
return {};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const registry: WorkspaceRegistry = {};
|
|
473
|
+
for (const [workspaceId, entry] of Object.entries(parsed as Record<string, unknown>)) {
|
|
474
|
+
const normalizedId = normalizeWorkspaceId(workspaceId);
|
|
475
|
+
if (!normalizedId) continue;
|
|
476
|
+
registry[normalizedId] = toWorkspaceRegistryEntry(entry);
|
|
477
|
+
}
|
|
478
|
+
return registry;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private writeWorkspaceRegistry(registry: WorkspaceRegistry): void {
|
|
482
|
+
const registryPath = this.getWorkspaceRegistryPath();
|
|
483
|
+
mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
484
|
+
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private persistWorkspaceMapping(workspaceId: string, apiKey: string): void {
|
|
488
|
+
const registry = this.readWorkspaceRegistry();
|
|
489
|
+
const existing = registry[workspaceId] ?? {};
|
|
490
|
+
registry[workspaceId] = {
|
|
491
|
+
...existing,
|
|
492
|
+
relaycastApiKey: apiKey,
|
|
493
|
+
relayfileUrl: existing.relayfileUrl,
|
|
494
|
+
createdAt: existing.createdAt ?? new Date().toISOString(),
|
|
495
|
+
agents: existing.agents ?? [],
|
|
496
|
+
};
|
|
497
|
+
this.writeWorkspaceRegistry(registry);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private findMappedWorkspaceIdByApiKey(apiKey: string): string | undefined {
|
|
501
|
+
const registry = this.readWorkspaceRegistry();
|
|
502
|
+
for (const [workspaceId, entry] of Object.entries(registry)) {
|
|
503
|
+
if (entry.relaycastApiKey === apiKey) {
|
|
504
|
+
return workspaceId;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private getResolvedWorkspaceId(): string | undefined {
|
|
511
|
+
return this.resolvedWorkspaceId ?? this.requestedWorkspaceId;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private getRelaycastBaseUrl(): string {
|
|
515
|
+
return (
|
|
516
|
+
this.relaycastBaseUrl ??
|
|
517
|
+
this.clientOptions.env?.RELAYCAST_BASE_URL ??
|
|
518
|
+
process.env.RELAYCAST_BASE_URL ??
|
|
519
|
+
'https://api.relaycast.dev'
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private applyWorkspaceEnv(workspaceId: string, apiKey: string): void {
|
|
524
|
+
const env: NodeJS.ProcessEnv = {
|
|
525
|
+
...(this.clientOptions.env ?? process.env),
|
|
526
|
+
RELAY_API_KEY: apiKey,
|
|
527
|
+
RELAYFILE_WORKSPACE: workspaceId,
|
|
528
|
+
RELAY_DEFAULT_WORKSPACE: workspaceId,
|
|
529
|
+
RELAY_WORKSPACE_ID: workspaceId,
|
|
530
|
+
RELAY_WORKSPACES_JSON: JSON.stringify([{ workspace_id: workspaceId, api_key: apiKey }]),
|
|
531
|
+
};
|
|
532
|
+
if (this.relaycastBaseUrl) {
|
|
533
|
+
env.RELAYCAST_BASE_URL = this.relaycastBaseUrl;
|
|
534
|
+
}
|
|
535
|
+
this.clientOptions.env = env;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private async createMappedRelaycastWorkspace(workspaceId: string): Promise<string> {
|
|
539
|
+
const created = await RelayCast.createWorkspace(
|
|
540
|
+
this.workspaceName ?? workspaceId,
|
|
541
|
+
this.getRelaycastBaseUrl()
|
|
542
|
+
);
|
|
543
|
+
return created.apiKey;
|
|
544
|
+
}
|
|
545
|
+
|
|
352
546
|
/**
|
|
353
547
|
* Subscribe to broker stderr output. Listener is wired immediately if the
|
|
354
548
|
* client is already started, otherwise it is attached when the client starts.
|
|
355
549
|
* Returns an unsubscribe function.
|
|
356
550
|
*/
|
|
357
551
|
onBrokerStderr(listener: (line: string) => void): () => void {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// Queue it: once ensureStarted completes, wire it up
|
|
362
|
-
let unsub: (() => void) | undefined;
|
|
363
|
-
const queuedUnsub = () => {
|
|
364
|
-
unsub?.();
|
|
552
|
+
this.stderrListeners.add(listener);
|
|
553
|
+
return () => {
|
|
554
|
+
this.stderrListeners.delete(listener);
|
|
365
555
|
};
|
|
366
|
-
// Use the start promise if one is pending
|
|
367
|
-
const promise = this.startPromise ?? this.ensureStarted();
|
|
368
|
-
promise
|
|
369
|
-
.then((c) => {
|
|
370
|
-
unsub = c.onBrokerStderr(listener);
|
|
371
|
-
})
|
|
372
|
-
.catch(() => {});
|
|
373
|
-
return queuedUnsub;
|
|
374
556
|
}
|
|
375
557
|
|
|
376
558
|
// ── Spawning ────────────────────────────────────────────────────────────
|
|
@@ -594,7 +776,7 @@ export class AgentRelay {
|
|
|
594
776
|
/** Pre-register a batch of agents with Relaycast before steps execute. */
|
|
595
777
|
async preflightAgents(agents: Array<{ name: string; cli: string }>): Promise<void> {
|
|
596
778
|
const client = await this.ensureStarted();
|
|
597
|
-
await client.
|
|
779
|
+
await client.preflight(agents);
|
|
598
780
|
}
|
|
599
781
|
|
|
600
782
|
/** List agents with PIDs from the broker (for worker registration). */
|
|
@@ -939,36 +1121,39 @@ export class AgentRelay {
|
|
|
939
1121
|
*/
|
|
940
1122
|
private async ensureRelaycastApiKey(): Promise<void> {
|
|
941
1123
|
if (this.relayApiKey) {
|
|
942
|
-
this.
|
|
1124
|
+
const workspaceId = this.getResolvedWorkspaceId();
|
|
1125
|
+
if (workspaceId) {
|
|
1126
|
+
this.applyWorkspaceEnv(workspaceId, this.relayApiKey);
|
|
1127
|
+
try { this.persistWorkspaceMapping(workspaceId, this.relayApiKey); } catch { /* non-critical */ }
|
|
1128
|
+
} else {
|
|
1129
|
+
this.wireRelaycastBaseUrl();
|
|
1130
|
+
}
|
|
943
1131
|
return;
|
|
944
1132
|
}
|
|
945
1133
|
|
|
946
1134
|
const envKey = this.clientOptions.env?.RELAY_API_KEY ?? process.env.RELAY_API_KEY;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
this.clientOptions.env.RELAY_API_KEY = envKey;
|
|
958
|
-
}
|
|
959
|
-
this.wireRelaycastBaseUrl();
|
|
1135
|
+
const requestedWorkspaceId = this.requestedWorkspaceId;
|
|
1136
|
+
if (requestedWorkspaceId) {
|
|
1137
|
+
const registry = this.readWorkspaceRegistry();
|
|
1138
|
+
const mappedKey = registry[requestedWorkspaceId]?.relaycastApiKey;
|
|
1139
|
+
const resolvedKey =
|
|
1140
|
+
mappedKey ?? envKey ?? (await this.createMappedRelaycastWorkspace(requestedWorkspaceId));
|
|
1141
|
+
this.relayApiKey = resolvedKey;
|
|
1142
|
+
this.resolvedWorkspaceId = requestedWorkspaceId;
|
|
1143
|
+
this.applyWorkspaceEnv(requestedWorkspaceId, resolvedKey);
|
|
1144
|
+
try { this.persistWorkspaceMapping(requestedWorkspaceId, resolvedKey); } catch { /* non-critical */ }
|
|
960
1145
|
return;
|
|
961
1146
|
}
|
|
962
1147
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (!this.clientOptions.env) {
|
|
968
|
-
this.clientOptions.env = { ...process.env };
|
|
969
|
-
}
|
|
1148
|
+
const resolvedWorkspaceId = envKey
|
|
1149
|
+
? (this.findMappedWorkspaceIdByApiKey(envKey) ?? generateWorkspaceId())
|
|
1150
|
+
: generateWorkspaceId();
|
|
1151
|
+
const resolvedKey = envKey ?? (await this.createMappedRelaycastWorkspace(resolvedWorkspaceId));
|
|
970
1152
|
|
|
971
|
-
this.
|
|
1153
|
+
this.relayApiKey = resolvedKey;
|
|
1154
|
+
this.resolvedWorkspaceId = resolvedWorkspaceId;
|
|
1155
|
+
this.applyWorkspaceEnv(resolvedWorkspaceId, resolvedKey);
|
|
1156
|
+
try { this.persistWorkspaceMapping(resolvedWorkspaceId, resolvedKey); } catch { /* non-critical */ }
|
|
972
1157
|
}
|
|
973
1158
|
|
|
974
1159
|
/** Inject relaycastBaseUrl into broker env. Explicit option wins over inherited env. */
|
|
@@ -983,19 +1168,38 @@ export class AgentRelay {
|
|
|
983
1168
|
if (this.startPromise) return this.startPromise;
|
|
984
1169
|
|
|
985
1170
|
this.startPromise = this.ensureRelaycastApiKey()
|
|
986
|
-
.then(() =>
|
|
1171
|
+
.then(() =>
|
|
1172
|
+
AgentRelayClient.spawn({
|
|
1173
|
+
...this.clientOptions,
|
|
1174
|
+
onStderr: (line) => {
|
|
1175
|
+
for (const listener of this.stderrListeners) {
|
|
1176
|
+
try {
|
|
1177
|
+
listener(line);
|
|
1178
|
+
} catch {
|
|
1179
|
+
/* ignore */
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
})
|
|
1184
|
+
)
|
|
987
1185
|
.then((c) => {
|
|
988
|
-
this.client = c;
|
|
989
|
-
this.startPromise = undefined;
|
|
990
1186
|
// Use the workspace key the broker actually connected with.
|
|
991
1187
|
// This ensures SDK and workers are always on the same workspace.
|
|
992
1188
|
if (c.workspaceKey) {
|
|
993
1189
|
this.relayApiKey = c.workspaceKey;
|
|
1190
|
+
const workspaceId = this.getResolvedWorkspaceId();
|
|
1191
|
+
if (workspaceId) {
|
|
1192
|
+
this.applyWorkspaceEnv(workspaceId, c.workspaceKey);
|
|
1193
|
+
try { this.persistWorkspaceMapping(workspaceId, c.workspaceKey); } catch { /* non-critical */ }
|
|
1194
|
+
}
|
|
994
1195
|
}
|
|
995
1196
|
this.wireEvents(c);
|
|
1197
|
+
this.client = c;
|
|
1198
|
+
this.startPromise = undefined;
|
|
996
1199
|
return c;
|
|
997
1200
|
})
|
|
998
1201
|
.catch((err) => {
|
|
1202
|
+
this.client = undefined;
|
|
999
1203
|
this.startPromise = undefined;
|
|
1000
1204
|
throw err;
|
|
1001
1205
|
});
|
|
@@ -1332,7 +1536,10 @@ export class AgentRelay {
|
|
|
1332
1536
|
async unsubscribe(channelsToRemove: string[]) {
|
|
1333
1537
|
await relay.unsubscribe({ agent: name, channels: channelsToRemove });
|
|
1334
1538
|
},
|
|
1335
|
-
onOutput(
|
|
1539
|
+
onOutput(
|
|
1540
|
+
callback: AgentOutputCallback,
|
|
1541
|
+
options?: { stream?: string; mode?: 'chunk' | 'structured' }
|
|
1542
|
+
): () => void {
|
|
1336
1543
|
let listeners = relay.outputListeners.get(name);
|
|
1337
1544
|
if (!listeners) {
|
|
1338
1545
|
listeners = new Set();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrokerTransport — HTTP/WS transport layer for communicating with the
|
|
3
|
+
* agent-relay broker. Used internally by AgentRelayClient.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - HTTP requests with API key auth and structured error parsing
|
|
7
|
+
* - WebSocket connection for real-time event streaming
|
|
8
|
+
* - Event buffering, replay, and query (mirrors stdio client behavior)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import WebSocket from 'ws';
|
|
12
|
+
import type { BrokerEvent } from './protocol.js';
|
|
13
|
+
|
|
14
|
+
export class AgentRelayProtocolError extends Error {
|
|
15
|
+
code: string;
|
|
16
|
+
retryable: boolean;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
|
|
19
|
+
constructor(payload: { code: string; message: string; retryable?: boolean; data?: unknown }) {
|
|
20
|
+
super(payload.message);
|
|
21
|
+
this.name = 'AgentRelayProtocolError';
|
|
22
|
+
this.code = payload.code;
|
|
23
|
+
this.retryable = payload.retryable ?? false;
|
|
24
|
+
this.data = payload.data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BrokerTransportOptions {
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
/** Timeout in ms for HTTP requests. Default: 30000. */
|
|
32
|
+
requestTimeoutMs?: number;
|
|
33
|
+
/** Maximum number of events to buffer in memory for queryEvents/getLastEvent */
|
|
34
|
+
maxBufferSize?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class BrokerTransport {
|
|
38
|
+
private readonly baseUrl: string;
|
|
39
|
+
private readonly apiKey?: string;
|
|
40
|
+
private readonly requestTimeoutMs: number;
|
|
41
|
+
private readonly maxBufferSize: number;
|
|
42
|
+
|
|
43
|
+
private ws: WebSocket | null = null;
|
|
44
|
+
private eventListeners = new Set<(event: BrokerEvent) => void>();
|
|
45
|
+
private eventBuffer: BrokerEvent[] = [];
|
|
46
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
private sinceSeq = 0;
|
|
48
|
+
private _connected = false;
|
|
49
|
+
private _intentionalClose = false;
|
|
50
|
+
|
|
51
|
+
constructor(options: BrokerTransportOptions) {
|
|
52
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
53
|
+
this.apiKey = options.apiKey;
|
|
54
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
55
|
+
this.maxBufferSize = options.maxBufferSize ?? 1000;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get connected(): boolean {
|
|
59
|
+
return this._connected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get wsUrl(): string {
|
|
63
|
+
return this.baseUrl.replace(/^http/, 'ws') + '/ws';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── HTTP ─────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
|
69
|
+
const headers = new Headers(init?.headers);
|
|
70
|
+
if (!headers.has('Content-Type')) {
|
|
71
|
+
headers.set('Content-Type', 'application/json');
|
|
72
|
+
}
|
|
73
|
+
if (this.apiKey) {
|
|
74
|
+
headers.set('X-API-Key', this.apiKey);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const signal = init?.signal ?? AbortSignal.timeout(this.requestTimeoutMs);
|
|
78
|
+
const res = await fetch(`${this.baseUrl}${path}`, { ...init, headers, signal });
|
|
79
|
+
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
let body: { code?: string; message?: string; error?: string } | undefined;
|
|
82
|
+
try {
|
|
83
|
+
body = (await res.json()) as { code?: string; message?: string; error?: string };
|
|
84
|
+
} catch {
|
|
85
|
+
// non-JSON error
|
|
86
|
+
}
|
|
87
|
+
throw new AgentRelayProtocolError({
|
|
88
|
+
code: body?.code ?? `http_${res.status}`,
|
|
89
|
+
message: body?.message ?? body?.error ?? res.statusText,
|
|
90
|
+
retryable: res.status >= 500,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return res.json() as Promise<T>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── WebSocket events ─────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
connect(sinceSeq?: number): void {
|
|
100
|
+
if (this.ws) return;
|
|
101
|
+
// Clear any pending reconnect timer to avoid duplicate connections
|
|
102
|
+
if (this.reconnectTimer) {
|
|
103
|
+
clearTimeout(this.reconnectTimer);
|
|
104
|
+
this.reconnectTimer = null;
|
|
105
|
+
}
|
|
106
|
+
this.sinceSeq = sinceSeq ?? this.sinceSeq;
|
|
107
|
+
this._connect();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private _connect(): void {
|
|
111
|
+
this._intentionalClose = false;
|
|
112
|
+
const url = `${this.wsUrl}?sinceSeq=${this.sinceSeq}`;
|
|
113
|
+
const headers: Record<string, string> = {};
|
|
114
|
+
if (this.apiKey) {
|
|
115
|
+
headers['X-API-Key'] = this.apiKey;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.ws = new WebSocket(url, { headers });
|
|
119
|
+
|
|
120
|
+
this.ws.on('open', () => {
|
|
121
|
+
this._connected = true;
|
|
122
|
+
if (this.reconnectTimer) {
|
|
123
|
+
clearTimeout(this.reconnectTimer);
|
|
124
|
+
this.reconnectTimer = null;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
this.ws.on('message', (data) => {
|
|
129
|
+
try {
|
|
130
|
+
const event = JSON.parse(data.toString()) as BrokerEvent & { seq?: number };
|
|
131
|
+
// Track sequence for replay on reconnect
|
|
132
|
+
if (typeof event.seq === 'number' && event.seq > this.sinceSeq) {
|
|
133
|
+
this.sinceSeq = event.seq;
|
|
134
|
+
}
|
|
135
|
+
// Buffer the event
|
|
136
|
+
this.eventBuffer.push(event);
|
|
137
|
+
if (this.eventBuffer.length > this.maxBufferSize) {
|
|
138
|
+
this.eventBuffer = this.eventBuffer.slice(-this.maxBufferSize);
|
|
139
|
+
}
|
|
140
|
+
// Notify listeners
|
|
141
|
+
for (const listener of this.eventListeners) {
|
|
142
|
+
try {
|
|
143
|
+
listener(event);
|
|
144
|
+
} catch {
|
|
145
|
+
// don't let a bad listener break the stream
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore non-JSON frames (ping/pong)
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.ws.on('close', () => {
|
|
154
|
+
this._connected = false;
|
|
155
|
+
this.ws = null;
|
|
156
|
+
// Auto-reconnect after 2s unless intentionally closed
|
|
157
|
+
if (!this._intentionalClose) {
|
|
158
|
+
this.reconnectTimer = setTimeout(() => this._connect(), 2000);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.ws.on('error', () => {
|
|
163
|
+
// error always followed by close
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
disconnect(): void {
|
|
168
|
+
this._intentionalClose = true;
|
|
169
|
+
if (this.reconnectTimer) {
|
|
170
|
+
clearTimeout(this.reconnectTimer);
|
|
171
|
+
this.reconnectTimer = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.ws) {
|
|
174
|
+
this.ws.close();
|
|
175
|
+
this.ws = null;
|
|
176
|
+
}
|
|
177
|
+
this._connected = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onEvent(listener: (event: BrokerEvent) => void): () => void {
|
|
181
|
+
this.eventListeners.add(listener);
|
|
182
|
+
return () => {
|
|
183
|
+
this.eventListeners.delete(listener);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
queryEvents(filter?: { kind?: string; name?: string; since?: number; limit?: number }): BrokerEvent[] {
|
|
188
|
+
let events = [...this.eventBuffer];
|
|
189
|
+
if (filter?.kind) {
|
|
190
|
+
events = events.filter((e) => e.kind === filter.kind);
|
|
191
|
+
}
|
|
192
|
+
if (filter?.name) {
|
|
193
|
+
events = events.filter((e) => 'name' in e && e.name === filter.name);
|
|
194
|
+
}
|
|
195
|
+
if (filter?.since !== undefined) {
|
|
196
|
+
const since = filter.since;
|
|
197
|
+
events = events.filter(
|
|
198
|
+
(e) => 'timestamp' in e && typeof e.timestamp === 'number' && e.timestamp >= since
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (filter?.limit !== undefined) {
|
|
202
|
+
events = events.slice(-filter.limit);
|
|
203
|
+
}
|
|
204
|
+
return events;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getLastEvent(kind: string, name?: string): BrokerEvent | undefined {
|
|
208
|
+
for (let i = this.eventBuffer.length - 1; i >= 0; i -= 1) {
|
|
209
|
+
const event = this.eventBuffer[i];
|
|
210
|
+
if (event.kind === kind && (!name || ('name' in event && event.name === name))) {
|
|
211
|
+
return event;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|