agent-relay 3.1.10 → 3.1.12

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.
Files changed (93) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +2 -2
  6. package/dist/src/cli/bootstrap.d.ts.map +1 -1
  7. package/dist/src/cli/bootstrap.js +2 -0
  8. package/dist/src/cli/bootstrap.js.map +1 -1
  9. package/dist/src/cli/commands/connect.d.ts +3 -0
  10. package/dist/src/cli/commands/connect.d.ts.map +1 -0
  11. package/dist/src/cli/commands/connect.js +18 -0
  12. package/dist/src/cli/commands/connect.js.map +1 -0
  13. package/dist/src/cli/lib/auth-ssh.d.ts.map +1 -1
  14. package/dist/src/cli/lib/auth-ssh.js +22 -270
  15. package/dist/src/cli/lib/auth-ssh.js.map +1 -1
  16. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  17. package/dist/src/cli/lib/broker-lifecycle.js +33 -0
  18. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  19. package/dist/src/cli/lib/connect-daytona.d.ts +15 -0
  20. package/dist/src/cli/lib/connect-daytona.d.ts.map +1 -0
  21. package/dist/src/cli/lib/connect-daytona.js +217 -0
  22. package/dist/src/cli/lib/connect-daytona.js.map +1 -0
  23. package/dist/src/cli/lib/ssh-interactive.d.ts +41 -0
  24. package/dist/src/cli/lib/ssh-interactive.d.ts.map +1 -0
  25. package/dist/src/cli/lib/ssh-interactive.js +320 -0
  26. package/dist/src/cli/lib/ssh-interactive.js.map +1 -0
  27. package/install.sh +2 -1
  28. package/package.json +13 -10
  29. package/packages/acp-bridge/package.json +2 -2
  30. package/packages/config/dist/cli-auth-config.d.ts +2 -0
  31. package/packages/config/dist/cli-auth-config.d.ts.map +1 -1
  32. package/packages/config/dist/cli-auth-config.js +1 -0
  33. package/packages/config/dist/cli-auth-config.js.map +1 -1
  34. package/packages/config/package.json +1 -1
  35. package/packages/config/src/cli-auth-config.ts +3 -0
  36. package/packages/hooks/package.json +4 -4
  37. package/packages/memory/package.json +2 -2
  38. package/packages/openclaw/dist/__tests__/gateway-control.test.js +13 -13
  39. package/packages/openclaw/dist/__tests__/gateway-control.test.js.map +1 -1
  40. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts +2 -0
  41. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts.map +1 -0
  42. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js +369 -0
  43. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js.map +1 -0
  44. package/packages/openclaw/dist/__tests__/gateway-threads.test.js +57 -16
  45. package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -1
  46. package/packages/openclaw/dist/__tests__/ws-client.test.js +2 -0
  47. package/packages/openclaw/dist/__tests__/ws-client.test.js.map +1 -1
  48. package/packages/openclaw/dist/config.d.ts +2 -0
  49. package/packages/openclaw/dist/config.d.ts.map +1 -1
  50. package/packages/openclaw/dist/config.js +99 -12
  51. package/packages/openclaw/dist/config.js.map +1 -1
  52. package/packages/openclaw/dist/gateway.d.ts +56 -2
  53. package/packages/openclaw/dist/gateway.d.ts.map +1 -1
  54. package/packages/openclaw/dist/gateway.js +819 -127
  55. package/packages/openclaw/dist/gateway.js.map +1 -1
  56. package/packages/openclaw/dist/runtime/openclaw-config.d.ts +2 -0
  57. package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -1
  58. package/packages/openclaw/dist/runtime/openclaw-config.js +1 -1
  59. package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -1
  60. package/packages/openclaw/dist/runtime/setup.d.ts +0 -2
  61. package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -1
  62. package/packages/openclaw/dist/runtime/setup.js +53 -8
  63. package/packages/openclaw/dist/runtime/setup.js.map +1 -1
  64. package/packages/openclaw/dist/types.d.ts +28 -0
  65. package/packages/openclaw/dist/types.d.ts.map +1 -1
  66. package/packages/openclaw/package.json +2 -2
  67. package/packages/openclaw/skill/SKILL.md +150 -44
  68. package/packages/openclaw/src/__tests__/gateway-control.test.ts +14 -14
  69. package/packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts +467 -0
  70. package/packages/openclaw/src/__tests__/gateway-threads.test.ts +73 -23
  71. package/packages/openclaw/src/__tests__/ws-client.test.ts +71 -51
  72. package/packages/openclaw/src/config.ts +121 -12
  73. package/packages/openclaw/src/gateway.ts +1155 -252
  74. package/packages/openclaw/src/runtime/openclaw-config.ts +3 -1
  75. package/packages/openclaw/src/runtime/setup.ts +57 -16
  76. package/packages/openclaw/src/types.ts +31 -0
  77. package/packages/openclaw/test/vitest.setup.ts +1 -0
  78. package/packages/policy/package.json +2 -2
  79. package/packages/sdk/dist/__tests__/unit.test.js +131 -129
  80. package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
  81. package/packages/sdk/dist/relay.js +1 -1
  82. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  83. package/packages/sdk/dist/workflows/runner.js +5 -3
  84. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  85. package/packages/sdk/package.json +2 -2
  86. package/packages/sdk/src/__tests__/unit.test.ts +142 -157
  87. package/packages/sdk/src/relay.ts +1 -1
  88. package/packages/sdk/src/workflows/runner.ts +12 -9
  89. package/packages/sdk-py/pyproject.toml +1 -1
  90. package/packages/telemetry/package.json +1 -1
  91. package/packages/trajectory/package.json +2 -2
  92. package/packages/user-directory/package.json +2 -2
  93. package/packages/utils/package.json +2 -2
@@ -4,15 +4,15 @@
4
4
  * Run:
5
5
  * npm run build && node --test dist/__tests__/unit.test.js
6
6
  */
7
- import assert from "node:assert/strict";
8
- import { join, sep } from "node:path";
9
- import { appendFile, mkdtemp, writeFile, rm } from "node:fs/promises";
10
- import { tmpdir } from "node:os";
11
- import { setTimeout as sleep } from "node:timers/promises";
12
- import test from "node:test";
7
+ import assert from 'node:assert/strict';
8
+ import { join, sep } from 'node:path';
9
+ import { appendFile, mkdtemp, writeFile, rm } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import { setTimeout as sleep } from 'node:timers/promises';
12
+ import test from 'node:test';
13
13
 
14
- import { AgentRelay, type Agent } from "../relay.js";
15
- import { followLogs, getLogs, listLoggedAgents, type LogFollowEvent } from "../logs.js";
14
+ import { AgentRelay, type Agent } from '../relay.js';
15
+ import { followLogs, getLogs, listLoggedAgents, type LogFollowEvent } from '../logs.js';
16
16
 
17
17
  // ── waitForAny ──────────────────────────────────────────────────────────────
18
18
 
@@ -22,99 +22,85 @@ interface FakeAgentControls {
22
22
  triggerIdle: () => void;
23
23
  }
24
24
 
25
- function makeFakeAgent(
26
- name: string,
27
- exitAfterMs?: number,
28
- ): Agent {
25
+ function makeFakeAgent(name: string, exitAfterMs?: number): Agent {
29
26
  return makeFakeAgentWithControls(name, exitAfterMs).agent;
30
27
  }
31
28
 
32
- function makeFakeAgentWithControls(
33
- name: string,
34
- exitAfterMs?: number,
35
- ): FakeAgentControls {
36
- let resolveExit: ((reason: "exited" | "released") => void) | undefined;
37
- const exitPromise = new Promise<"exited" | "released">((resolve) => {
29
+ function makeFakeAgentWithControls(name: string, exitAfterMs?: number): FakeAgentControls {
30
+ let resolveExit: ((reason: 'exited' | 'released') => void) | undefined;
31
+ const exitPromise = new Promise<'exited' | 'released'>((resolve) => {
38
32
  resolveExit = resolve;
39
33
  });
40
34
 
41
- let resolveIdle: ((reason: "idle" | "timeout" | "exited") => void) | undefined;
42
- let idlePromise: Promise<"idle" | "timeout" | "exited"> | undefined;
35
+ let resolveIdle: ((reason: 'idle' | 'timeout' | 'exited') => void) | undefined;
36
+ let idlePromise: Promise<'idle' | 'timeout' | 'exited'> | undefined;
43
37
 
44
38
  function makeIdlePromise() {
45
- idlePromise = new Promise<"idle" | "timeout" | "exited">((resolve) => {
39
+ idlePromise = new Promise<'idle' | 'timeout' | 'exited'>((resolve) => {
46
40
  resolveIdle = resolve;
47
41
  });
48
42
  }
49
43
 
50
44
  if (exitAfterMs !== undefined) {
51
- setTimeout(() => resolveExit?.("exited"), exitAfterMs);
45
+ setTimeout(() => resolveExit?.('exited'), exitAfterMs);
52
46
  }
53
47
 
54
48
  const agent: Agent = {
55
49
  name,
56
- runtime: "pty",
57
- channels: ["general"],
58
- status: "ready",
50
+ runtime: 'pty',
51
+ channels: ['general'],
52
+ status: 'ready',
59
53
  exitCode: undefined,
60
54
  exitSignal: undefined,
61
55
  async release() {
62
- resolveExit?.("released");
56
+ resolveExit?.('released');
63
57
  },
64
58
  waitForReady(timeoutMs?: number) {
65
59
  if (timeoutMs === 0) {
66
- return Promise.reject(
67
- new Error(`Timed out waiting for worker_ready for '${name}' after 0ms`),
68
- );
60
+ return Promise.reject(new Error(`Timed out waiting for worker_ready for '${name}' after 0ms`));
69
61
  }
70
62
  return Promise.resolve();
71
63
  },
72
64
  waitForExit(timeoutMs?: number) {
73
- if (timeoutMs === 0) return Promise.resolve("timeout" as const);
65
+ if (timeoutMs === 0) return Promise.resolve('timeout' as const);
74
66
  if (timeoutMs !== undefined) {
75
67
  return Promise.race([
76
68
  exitPromise,
77
- new Promise<"timeout">((resolve) =>
78
- setTimeout(() => resolve("timeout"), timeoutMs),
79
- ),
69
+ new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs)),
80
70
  ]);
81
71
  }
82
72
  return exitPromise;
83
73
  },
84
74
  waitForIdle(timeoutMs?: number) {
85
75
  makeIdlePromise();
86
- if (timeoutMs === 0) return Promise.resolve("timeout" as const);
76
+ if (timeoutMs === 0) return Promise.resolve('timeout' as const);
87
77
  if (timeoutMs !== undefined) {
88
78
  return Promise.race([
89
79
  idlePromise!,
90
- new Promise<"timeout">((resolve) =>
91
- setTimeout(() => resolve("timeout"), timeoutMs),
92
- ),
80
+ new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs)),
93
81
  ]);
94
82
  }
95
83
  return idlePromise!;
96
84
  },
97
85
  async sendMessage() {
98
- return { eventId: "fake", from: name, to: "", text: "" };
86
+ return { eventId: 'fake', from: name, to: '', text: '' };
99
87
  },
100
- onOutput(
101
- _callback: ((chunk: string) => void) | ((data: { stream: string; chunk: string }) => void),
102
- ) {
88
+ onOutput(_callback: ((chunk: string) => void) | ((data: { stream: string; chunk: string }) => void)) {
103
89
  return () => {};
104
90
  },
105
91
  };
106
92
 
107
93
  return {
108
94
  agent,
109
- triggerExit: () => resolveExit?.("exited"),
110
- triggerIdle: () => resolveIdle?.("idle"),
95
+ triggerExit: () => resolveExit?.('exited'),
96
+ triggerIdle: () => resolveIdle?.('idle'),
111
97
  };
112
98
  }
113
99
 
114
100
  async function waitForLogEvent(
115
101
  events: LogFollowEvent[],
116
102
  predicate: (event: LogFollowEvent) => boolean,
117
- timeoutMs = 2_000,
103
+ timeoutMs = 2_000
118
104
  ): Promise<LogFollowEvent> {
119
105
  const startedAt = Date.now();
120
106
  while (Date.now() - startedAt < timeoutMs) {
@@ -124,118 +110,117 @@ async function waitForLogEvent(
124
110
  }
125
111
  await sleep(20);
126
112
  }
127
- throw new Error("Timed out waiting for log follow event");
113
+ throw new Error('Timed out waiting for log follow event');
128
114
  }
129
115
 
130
- test("waitForAny: returns first agent to exit", async () => {
131
- const fast = makeFakeAgent("fast", 50);
132
- const slow = makeFakeAgent("slow", 5_000);
116
+ test('waitForAny: returns first agent to exit', async () => {
117
+ const fast = makeFakeAgent('fast', 50);
118
+ const slow = makeFakeAgent('slow', 5_000);
133
119
 
134
120
  const { agent, result } = await AgentRelay.waitForAny([fast, slow], 3_000);
135
- assert.equal(agent.name, "fast");
136
- assert.equal(result, "exited");
121
+ assert.equal(agent.name, 'fast');
122
+ assert.equal(result, 'exited');
137
123
  });
138
124
 
139
- test("waitForAny: returns timeout when no agent exits", async () => {
140
- const a = makeFakeAgent("a");
141
- const b = makeFakeAgent("b");
125
+ test('waitForAny: returns timeout when no agent exits', async () => {
126
+ const a = makeFakeAgent('a');
127
+ const b = makeFakeAgent('b');
142
128
 
143
129
  const { result } = await AgentRelay.waitForAny([a, b], 100);
144
- assert.equal(result, "timeout");
130
+ assert.equal(result, 'timeout');
145
131
  });
146
132
 
147
- test("waitForAny: handles released agent", async () => {
148
- const agent = makeFakeAgent("releasable");
133
+ test('waitForAny: handles released agent', async () => {
134
+ const agent = makeFakeAgent('releasable');
149
135
 
150
136
  // Release after 50ms
151
137
  setTimeout(() => agent.release(), 50);
152
138
 
153
139
  const { agent: resolved, result } = await AgentRelay.waitForAny([agent], 3_000);
154
- assert.equal(resolved.name, "releasable");
155
- assert.equal(result, "released");
140
+ assert.equal(resolved.name, 'releasable');
141
+ assert.equal(result, 'released');
156
142
  });
157
143
 
158
- test("waitForAny: throws on empty agents array", async () => {
159
- await assert.rejects(
160
- () => AgentRelay.waitForAny([]),
161
- { message: "waitForAny requires at least one agent" },
162
- );
144
+ test('waitForAny: throws on empty agents array', async () => {
145
+ await assert.rejects(() => AgentRelay.waitForAny([]), {
146
+ message: 'waitForAny requires at least one agent',
147
+ });
163
148
  });
164
149
 
165
150
  // ── getLogs ──────────────────────────────────────────────────────────────────
166
151
 
167
- test("getLogs: rejects path traversal", async () => {
168
- const result = await getLogs("../../etc/passwd", {
169
- logsDir: "/tmp/test-logs",
152
+ test('getLogs: rejects path traversal', async () => {
153
+ const result = await getLogs('../../etc/passwd', {
154
+ logsDir: '/tmp/test-logs',
170
155
  });
171
156
  assert.equal(result.found, false);
172
- assert.equal(result.content, "");
157
+ assert.equal(result.content, '');
173
158
  });
174
159
 
175
- test("getLogs: returns not found for missing agent", async () => {
176
- const dir = await mkdtemp(join(tmpdir(), "relay-test-logs-"));
160
+ test('getLogs: returns not found for missing agent', async () => {
161
+ const dir = await mkdtemp(join(tmpdir(), 'relay-test-logs-'));
177
162
 
178
163
  try {
179
- const result = await getLogs("nonexistent", { logsDir: dir });
164
+ const result = await getLogs('nonexistent', { logsDir: dir });
180
165
  assert.equal(result.found, false);
181
- assert.equal(result.content, "");
166
+ assert.equal(result.content, '');
182
167
  } finally {
183
168
  await rm(dir, { recursive: true, force: true });
184
169
  }
185
170
  });
186
171
 
187
- test("getLogs: reads content from log file", async () => {
188
- const dir = await mkdtemp(join(tmpdir(), "relay-test-logs-"));
172
+ test('getLogs: reads content from log file', async () => {
173
+ const dir = await mkdtemp(join(tmpdir(), 'relay-test-logs-'));
189
174
 
190
175
  try {
191
- const logContent = "line1\nline2\nline3\n";
192
- await writeFile(join(dir, "TestAgent.log"), logContent);
176
+ const logContent = 'line1\nline2\nline3\n';
177
+ await writeFile(join(dir, 'TestAgent.log'), logContent);
193
178
 
194
- const result = await getLogs("TestAgent", { logsDir: dir, lines: 2 });
179
+ const result = await getLogs('TestAgent', { logsDir: dir, lines: 2 });
195
180
  assert.equal(result.found, true);
196
- assert.equal(result.content, "line2\nline3");
181
+ assert.equal(result.content, 'line2\nline3');
197
182
  assert.equal(result.lineCount, 2);
198
183
  } finally {
199
184
  await rm(dir, { recursive: true, force: true });
200
185
  }
201
186
  });
202
187
 
203
- test("listLoggedAgents: lists agent names from log files", async () => {
204
- const dir = await mkdtemp(join(tmpdir(), "relay-test-logs-"));
188
+ test('listLoggedAgents: lists agent names from log files', async () => {
189
+ const dir = await mkdtemp(join(tmpdir(), 'relay-test-logs-'));
205
190
 
206
191
  try {
207
- await writeFile(join(dir, "Alice.log"), "hello\n");
208
- await writeFile(join(dir, "Bob.log"), "world\n");
209
- await writeFile(join(dir, "not-a-log.txt"), "skip\n");
192
+ await writeFile(join(dir, 'Alice.log'), 'hello\n');
193
+ await writeFile(join(dir, 'Bob.log'), 'world\n');
194
+ await writeFile(join(dir, 'not-a-log.txt'), 'skip\n');
210
195
 
211
196
  const agents = await listLoggedAgents(dir);
212
- assert.ok(agents.includes("Alice"));
213
- assert.ok(agents.includes("Bob"));
214
- assert.ok(!agents.includes("not-a-log"));
197
+ assert.ok(agents.includes('Alice'));
198
+ assert.ok(agents.includes('Bob'));
199
+ assert.ok(!agents.includes('not-a-log'));
215
200
  } finally {
216
201
  await rm(dir, { recursive: true, force: true });
217
202
  }
218
203
  });
219
204
 
220
- test("listLoggedAgents: returns empty for missing directory", async () => {
221
- const agents = await listLoggedAgents("/tmp/definitely-nonexistent-dir");
205
+ test('listLoggedAgents: returns empty for missing directory', async () => {
206
+ const agents = await listLoggedAgents('/tmp/definitely-nonexistent-dir');
222
207
  assert.deepEqual(agents, []);
223
208
  });
224
209
 
225
- test("followLogs: emits error for missing logs by default", async () => {
226
- const dir = await mkdtemp(join(tmpdir(), "relay-test-follow-"));
210
+ test('followLogs: emits error for missing logs by default', async () => {
211
+ const dir = await mkdtemp(join(tmpdir(), 'relay-test-follow-'));
227
212
  const events: LogFollowEvent[] = [];
228
213
 
229
214
  try {
230
- const handle = followLogs("MissingAgent", {
215
+ const handle = followLogs('MissingAgent', {
231
216
  logsDir: dir,
232
217
  pollMs: 50,
233
218
  onEvent: (event) => events.push(event),
234
219
  });
235
220
 
236
- const event = await waitForLogEvent(events, (item) => item.type === "error");
237
- assert.equal(event.type, "error");
238
- if (event.type === "error") {
221
+ const event = await waitForLogEvent(events, (item) => item.type === 'error');
222
+ assert.equal(event.type, 'error');
223
+ if (event.type === 'error') {
239
224
  assert.match(event.error, /No local logs/);
240
225
  assert.ok(Array.isArray(event.availableAgents));
241
226
  }
@@ -246,75 +231,75 @@ test("followLogs: emits error for missing logs by default", async () => {
246
231
  }
247
232
  });
248
233
 
249
- test("followLogs: emits history then incremental log content", async () => {
250
- const dir = await mkdtemp(join(tmpdir(), "relay-test-follow-"));
251
- const logFile = join(dir, "Worker.log");
234
+ test('followLogs: emits history then incremental log content', async () => {
235
+ const dir = await mkdtemp(join(tmpdir(), 'relay-test-follow-'));
236
+ const logFile = join(dir, 'Worker.log');
252
237
  const events: LogFollowEvent[] = [];
253
238
 
254
239
  try {
255
- await writeFile(logFile, "line1\nline2\n");
240
+ await writeFile(logFile, 'line1\nline2\n');
256
241
 
257
- const handle = followLogs("Worker", {
242
+ const handle = followLogs('Worker', {
258
243
  logsDir: dir,
259
244
  historyLines: 1,
260
245
  pollMs: 50,
261
246
  onEvent: (event) => events.push(event),
262
247
  });
263
248
 
264
- const historyEvent = await waitForLogEvent(events, (item) => item.type === "history");
265
- assert.equal(historyEvent.type, "history");
266
- if (historyEvent.type === "history") {
267
- assert.deepEqual(historyEvent.lines, ["line2"]);
249
+ const historyEvent = await waitForLogEvent(events, (item) => item.type === 'history');
250
+ assert.equal(historyEvent.type, 'history');
251
+ if (historyEvent.type === 'history') {
252
+ assert.deepEqual(historyEvent.lines, ['line2']);
268
253
  }
269
254
 
270
- await appendFile(logFile, "line3\nline4\n");
255
+ await appendFile(logFile, 'line3\nline4\n');
271
256
  const deltaEvent = await waitForLogEvent(
272
257
  events,
273
- (item) => item.type === "log" && item.content.includes("line3"),
258
+ (item) => item.type === 'log' && item.content.includes('line3')
274
259
  );
275
- assert.equal(deltaEvent.type, "log");
276
- if (deltaEvent.type === "log") {
260
+ assert.equal(deltaEvent.type, 'log');
261
+ if (deltaEvent.type === 'log') {
277
262
  assert.match(deltaEvent.content, /line3/);
278
263
  assert.match(deltaEvent.content, /line4/);
279
264
  }
280
265
 
281
- const logEventsBeforeStop = events.filter((item) => item.type === "log").length;
266
+ const logEventsBeforeStop = events.filter((item) => item.type === 'log').length;
282
267
  handle.unsubscribe();
283
- await appendFile(logFile, "line5\n");
268
+ await appendFile(logFile, 'line5\n');
284
269
  await sleep(120);
285
- const logEventsAfterStop = events.filter((item) => item.type === "log").length;
270
+ const logEventsAfterStop = events.filter((item) => item.type === 'log').length;
286
271
  assert.equal(logEventsAfterStop, logEventsBeforeStop);
287
272
  } finally {
288
273
  await rm(dir, { recursive: true, force: true });
289
274
  }
290
275
  });
291
276
 
292
- test("followLogs: allowMissing keeps stream open until file appears", async () => {
293
- const dir = await mkdtemp(join(tmpdir(), "relay-test-follow-"));
294
- const logFile = join(dir, "LateWorker.log");
277
+ test('followLogs: allowMissing keeps stream open until file appears', async () => {
278
+ const dir = await mkdtemp(join(tmpdir(), 'relay-test-follow-'));
279
+ const logFile = join(dir, 'LateWorker.log');
295
280
  const events: LogFollowEvent[] = [];
296
281
 
297
282
  try {
298
- const handle = followLogs("LateWorker", {
283
+ const handle = followLogs('LateWorker', {
299
284
  logsDir: dir,
300
285
  allowMissing: true,
301
286
  pollMs: 50,
302
287
  onEvent: (event) => events.push(event),
303
288
  });
304
289
 
305
- const historyEvent = await waitForLogEvent(events, (item) => item.type === "history");
306
- assert.equal(historyEvent.type, "history");
307
- if (historyEvent.type === "history") {
290
+ const historyEvent = await waitForLogEvent(events, (item) => item.type === 'history');
291
+ assert.equal(historyEvent.type, 'history');
292
+ if (historyEvent.type === 'history') {
308
293
  assert.deepEqual(historyEvent.lines, []);
309
294
  }
310
295
 
311
- await writeFile(logFile, "boot\n");
296
+ await writeFile(logFile, 'boot\n');
312
297
  const deltaEvent = await waitForLogEvent(
313
298
  events,
314
- (item) => item.type === "log" && item.content.includes("boot"),
299
+ (item) => item.type === 'log' && item.content.includes('boot')
315
300
  );
316
- assert.equal(deltaEvent.type, "log");
317
- if (deltaEvent.type === "log") {
301
+ assert.equal(deltaEvent.type, 'log');
302
+ if (deltaEvent.type === 'log') {
318
303
  assert.match(deltaEvent.content, /boot/);
319
304
  }
320
305
 
@@ -326,22 +311,22 @@ test("followLogs: allowMissing keeps stream open until file appears", async () =
326
311
 
327
312
  // ── waitForIdle ────────────────────────────────────────────────────────────
328
313
 
329
- test("waitForIdle: resolves with idle when agent goes idle", async () => {
330
- const { agent, triggerIdle } = makeFakeAgentWithControls("worker");
314
+ test('waitForIdle: resolves with idle when agent goes idle', async () => {
315
+ const { agent, triggerIdle } = makeFakeAgentWithControls('worker');
331
316
  const promise = agent.waitForIdle(5_000);
332
317
  setTimeout(() => triggerIdle(), 20);
333
318
  const result = await promise;
334
- assert.equal(result, "idle");
319
+ assert.equal(result, 'idle');
335
320
  });
336
321
 
337
- test("waitForIdle: resolves with timeout when time elapses", async () => {
338
- const { agent } = makeFakeAgentWithControls("worker");
322
+ test('waitForIdle: resolves with timeout when time elapses', async () => {
323
+ const { agent } = makeFakeAgentWithControls('worker');
339
324
  const result = await agent.waitForIdle(50);
340
- assert.equal(result, "timeout");
325
+ assert.equal(result, 'timeout');
341
326
  });
342
327
 
343
- test("waitForIdle: resolves with exited when agent exits before idle", async () => {
344
- const { agent, triggerExit } = makeFakeAgentWithControls("worker");
328
+ test('waitForIdle: resolves with exited when agent exits before idle', async () => {
329
+ const { agent, triggerExit } = makeFakeAgentWithControls('worker');
345
330
  const idlePromise = agent.waitForIdle(5_000);
346
331
 
347
332
  // Simulate exit resolving the idle promise (as relay.ts wireEvents does)
@@ -356,80 +341,80 @@ test("waitForIdle: resolves with exited when agent exits before idle", async ()
356
341
  // wireEvents handler resolves idle resolvers on exit.
357
342
  // For the mock, we can test the timeout path instead.
358
343
  const result = await agent.waitForIdle(100);
359
- assert.equal(result, "timeout");
344
+ assert.equal(result, 'timeout');
360
345
  });
361
346
 
362
- test("waitForIdle: returns timeout immediately with timeoutMs=0", async () => {
363
- const { agent } = makeFakeAgentWithControls("worker");
347
+ test('waitForIdle: returns timeout immediately with timeoutMs=0', async () => {
348
+ const { agent } = makeFakeAgentWithControls('worker');
364
349
  const result = await agent.waitForIdle(0);
365
- assert.equal(result, "timeout");
350
+ assert.equal(result, 'timeout');
366
351
  });
367
352
 
368
- test("waitForIdle: idle resolves before timeout", async () => {
369
- const { agent, triggerIdle } = makeFakeAgentWithControls("worker");
353
+ test('waitForIdle: idle resolves before timeout', async () => {
354
+ const { agent, triggerIdle } = makeFakeAgentWithControls('worker');
370
355
  // Trigger idle almost immediately, with a long timeout
371
356
  const promise = agent.waitForIdle(5_000);
372
357
  setTimeout(() => triggerIdle(), 10);
373
358
  const result = await promise;
374
- assert.equal(result, "idle");
359
+ assert.equal(result, 'idle');
375
360
  });
376
361
  // ── agent.status ────────────────────────────────────────────────────────────
377
362
 
378
- test("agent.status: mock agent has ready status", () => {
379
- const { agent } = makeFakeAgentWithControls("worker");
380
- assert.equal(agent.status, "ready");
363
+ test('agent.status: mock agent has ready status', () => {
364
+ const { agent } = makeFakeAgentWithControls('worker');
365
+ assert.equal(agent.status, 'ready');
381
366
  });
382
367
 
383
368
  // ── agent.onOutput ──────────────────────────────────────────────────────────
384
369
 
385
- test("agent.onOutput: mock returns unsubscribe function", () => {
386
- const { agent } = makeFakeAgentWithControls("worker");
370
+ test('agent.onOutput: mock returns unsubscribe function', () => {
371
+ const { agent } = makeFakeAgentWithControls('worker');
387
372
  const chunks: string[] = [];
388
373
  const unsub = agent.onOutput(({ chunk }: { stream: string; chunk: string }) => chunks.push(chunk));
389
- assert.equal(typeof unsub, "function");
374
+ assert.equal(typeof unsub, 'function');
390
375
  unsub();
391
376
  });
392
377
 
393
378
  // ── AgentRelay.workspaceKey / observerUrl ────────────────────────────────────
394
379
  // These tests verify the getter logic without a running broker.
395
380
 
396
- test("workspaceKey: undefined before relay starts", () => {
397
- const relay = new AgentRelay({ channels: ["general"] });
381
+ test('workspaceKey: undefined before relay starts', () => {
382
+ const relay = new AgentRelay({ channels: ['general'] });
398
383
  assert.equal(relay.workspaceKey, undefined);
399
384
  });
400
385
 
401
- test("observerUrl: undefined before relay starts", () => {
402
- const relay = new AgentRelay({ channels: ["general"] });
386
+ test('observerUrl: undefined before relay starts', () => {
387
+ const relay = new AgentRelay({ channels: ['general'] });
403
388
  assert.equal(relay.observerUrl, undefined);
404
389
  });
405
390
 
406
- test("observerUrl: returns correct URL format when workspaceKey is set", () => {
407
- const relay = new AgentRelay({ channels: ["general"] });
391
+ test('observerUrl: returns correct URL format when workspaceKey is set', () => {
392
+ const relay = new AgentRelay({ channels: ['general'] });
408
393
  // Simulate the relayApiKey being set (as ensureStarted() does after hello_ack).
409
- (relay as unknown as { relayApiKey: string }).relayApiKey = "rk_live_test123";
394
+ (relay as unknown as { relayApiKey: string }).relayApiKey = 'rk_live_test123';
410
395
  const url = relay.observerUrl;
411
- assert.ok(url, "observerUrl should be defined after key is set");
396
+ assert.ok(url, 'observerUrl should be defined after key is set');
412
397
  assert.ok(
413
- url!.startsWith("https://observer.relaycast.dev/?key="),
414
- `observerUrl should start with observer base URL, got: ${url}`,
398
+ url!.startsWith('https://agentrelay.dev/observer?key='),
399
+ `observerUrl should start with observer base URL, got: ${url}`
415
400
  );
416
- assert.ok(url!.includes("rk_live_test123"), "observerUrl should include the workspace key");
417
- assert.equal(relay.workspaceKey, "rk_live_test123");
401
+ assert.ok(url!.includes('rk_live_test123'), 'observerUrl should include the workspace key');
402
+ assert.equal(relay.workspaceKey, 'rk_live_test123');
418
403
  });
419
404
 
420
- test("ensureRelaycastApiKey: env key propagates to clientOptions.env", () => {
405
+ test('ensureRelaycastApiKey: env key propagates to clientOptions.env', () => {
421
406
  // When RELAY_API_KEY is in options.env, it must be preserved and passed
422
407
  // to the broker subprocess. Previously, if clientOptions.env was set via
423
408
  // the constructor, it would be used as-is without adding process.env
424
409
  // (which is correct). If clientOptions.env was undefined, we must populate
425
410
  // it so the broker gets the key AND PATH etc.
426
411
  const relay = new AgentRelay({
427
- channels: ["general"],
428
- env: { RELAY_API_KEY: "rk_live_from_options", PATH: "/usr/bin" },
412
+ channels: ['general'],
413
+ env: { RELAY_API_KEY: 'rk_live_from_options', PATH: '/usr/bin' },
429
414
  });
430
415
  // The key should be accessible via getter immediately (read from options.env).
431
416
  // Note: workspaceKey is only set after ensureStarted() runs, but we can
432
417
  // verify the env is configured correctly via the private field.
433
418
  const opts = (relay as unknown as { clientOptions: { env?: Record<string, string> } }).clientOptions;
434
- assert.equal(opts.env?.RELAY_API_KEY, "rk_live_from_options");
419
+ assert.equal(opts.env?.RELAY_API_KEY, 'rk_live_from_options');
435
420
  });
@@ -258,7 +258,7 @@ export class AgentRelay {
258
258
  /** Observer URL for the auto-created workspace (available after first spawn). */
259
259
  get observerUrl(): string | undefined {
260
260
  if (!this.relayApiKey) return undefined;
261
- return `https://observer.relaycast.dev/?key=${this.relayApiKey}`;
261
+ return `https://agentrelay.dev/observer?key=${this.relayApiKey}`;
262
262
  }
263
263
 
264
264
  // Shorthand spawners
@@ -281,13 +281,15 @@ export class WorkflowRunner {
281
281
  method: 'POST',
282
282
  headers: { 'content-type': 'application/json' },
283
283
  body: JSON.stringify({ apiKey }),
284
- }).then((res) => {
285
- if (!res.ok) {
286
- console.warn(`[WorkflowRunner] dashboard key push failed: HTTP ${res.status}`);
287
- }
288
- }).catch(() => {
289
- // Dashboard not running — silently ignore.
290
- });
284
+ })
285
+ .then((res) => {
286
+ if (!res.ok) {
287
+ console.warn(`[WorkflowRunner] dashboard key push failed: HTTP ${res.status}`);
288
+ }
289
+ })
290
+ .catch(() => {
291
+ // Dashboard not running — silently ignore.
292
+ });
291
293
  }
292
294
 
293
295
  private getRelayEnv(): NodeJS.ProcessEnv | undefined {
@@ -1169,7 +1171,7 @@ export class WorkflowRunner {
1169
1171
  this.log('API key resolved');
1170
1172
  if (this.relayApiKeyAutoCreated && this.relayApiKey) {
1171
1173
  this.log(`Workspace created — follow this run in Relaycast:`);
1172
- this.log(` Observer: https://observer.relaycast.dev/?key=${this.relayApiKey}`);
1174
+ this.log(` Observer: https://agentrelay.dev/observer?key=${this.relayApiKey}`);
1173
1175
  this.log(` Channel: ${channel}`);
1174
1176
  }
1175
1177
 
@@ -1738,7 +1740,8 @@ export class WorkflowRunner {
1738
1740
  if (failOnError && result.exitCode !== 0) {
1739
1741
  throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
1740
1742
  }
1741
- const output = step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
1743
+ const output =
1744
+ step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
1742
1745
 
1743
1746
  // Mark completed
1744
1747
  state.row.status = 'completed';
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.1.10"
7
+ version = "3.1.12"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.1.10",
3
+ "version": "3.1.12",
4
4
  "description": "Anonymous telemetry for Agent Relay usage analytics",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/trajectory",
3
- "version": "3.1.10",
3
+ "version": "3.1.12",
4
4
  "description": "Trajectory integration utilities (trail/PDERO) for Relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/config": "3.1.10"
25
+ "@agent-relay/config": "3.1.12"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",