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.
- 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 +2 -2
- 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/connect.d.ts +3 -0
- package/dist/src/cli/commands/connect.d.ts.map +1 -0
- package/dist/src/cli/commands/connect.js +18 -0
- package/dist/src/cli/commands/connect.js.map +1 -0
- package/dist/src/cli/lib/auth-ssh.d.ts.map +1 -1
- package/dist/src/cli/lib/auth-ssh.js +22 -270
- package/dist/src/cli/lib/auth-ssh.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +33 -0
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/connect-daytona.d.ts +15 -0
- package/dist/src/cli/lib/connect-daytona.d.ts.map +1 -0
- package/dist/src/cli/lib/connect-daytona.js +217 -0
- package/dist/src/cli/lib/connect-daytona.js.map +1 -0
- package/dist/src/cli/lib/ssh-interactive.d.ts +41 -0
- package/dist/src/cli/lib/ssh-interactive.d.ts.map +1 -0
- package/dist/src/cli/lib/ssh-interactive.js +320 -0
- package/dist/src/cli/lib/ssh-interactive.js.map +1 -0
- package/install.sh +2 -1
- package/package.json +13 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/dist/cli-auth-config.d.ts +2 -0
- package/packages/config/dist/cli-auth-config.d.ts.map +1 -1
- package/packages/config/dist/cli-auth-config.js +1 -0
- package/packages/config/dist/cli-auth-config.js.map +1 -1
- package/packages/config/package.json +1 -1
- package/packages/config/src/cli-auth-config.ts +3 -0
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/dist/__tests__/gateway-control.test.js +13 -13
- package/packages/openclaw/dist/__tests__/gateway-control.test.js.map +1 -1
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js +369 -0
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js +57 -16
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -1
- package/packages/openclaw/dist/__tests__/ws-client.test.js +2 -0
- package/packages/openclaw/dist/__tests__/ws-client.test.js.map +1 -1
- package/packages/openclaw/dist/config.d.ts +2 -0
- package/packages/openclaw/dist/config.d.ts.map +1 -1
- package/packages/openclaw/dist/config.js +99 -12
- package/packages/openclaw/dist/config.js.map +1 -1
- package/packages/openclaw/dist/gateway.d.ts +56 -2
- package/packages/openclaw/dist/gateway.d.ts.map +1 -1
- package/packages/openclaw/dist/gateway.js +819 -127
- package/packages/openclaw/dist/gateway.js.map +1 -1
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts +2 -0
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -1
- package/packages/openclaw/dist/runtime/openclaw-config.js +1 -1
- package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -1
- package/packages/openclaw/dist/runtime/setup.d.ts +0 -2
- package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -1
- package/packages/openclaw/dist/runtime/setup.js +53 -8
- package/packages/openclaw/dist/runtime/setup.js.map +1 -1
- package/packages/openclaw/dist/types.d.ts +28 -0
- package/packages/openclaw/dist/types.d.ts.map +1 -1
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +150 -44
- package/packages/openclaw/src/__tests__/gateway-control.test.ts +14 -14
- package/packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts +467 -0
- package/packages/openclaw/src/__tests__/gateway-threads.test.ts +73 -23
- package/packages/openclaw/src/__tests__/ws-client.test.ts +71 -51
- package/packages/openclaw/src/config.ts +121 -12
- package/packages/openclaw/src/gateway.ts +1155 -252
- package/packages/openclaw/src/runtime/openclaw-config.ts +3 -1
- package/packages/openclaw/src/runtime/setup.ts +57 -16
- package/packages/openclaw/src/types.ts +31 -0
- package/packages/openclaw/test/vitest.setup.ts +1 -0
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/__tests__/unit.test.js +131 -129
- package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/sdk/dist/relay.js +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +5 -3
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +142 -157
- package/packages/sdk/src/relay.ts +1 -1
- package/packages/sdk/src/workflows/runner.ts +12 -9
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- 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
|
|
8
|
-
import { join, sep } from
|
|
9
|
-
import { appendFile, mkdtemp, writeFile, rm } from
|
|
10
|
-
import { tmpdir } from
|
|
11
|
-
import { setTimeout as sleep } from
|
|
12
|
-
import test from
|
|
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
|
|
15
|
-
import { followLogs, getLogs, listLoggedAgents, type LogFollowEvent } from
|
|
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
|
-
|
|
34
|
-
|
|
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:
|
|
42
|
-
let idlePromise: Promise<
|
|
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<
|
|
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?.(
|
|
45
|
+
setTimeout(() => resolveExit?.('exited'), exitAfterMs);
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
const agent: Agent = {
|
|
55
49
|
name,
|
|
56
|
-
runtime:
|
|
57
|
-
channels: [
|
|
58
|
-
status:
|
|
50
|
+
runtime: 'pty',
|
|
51
|
+
channels: ['general'],
|
|
52
|
+
status: 'ready',
|
|
59
53
|
exitCode: undefined,
|
|
60
54
|
exitSignal: undefined,
|
|
61
55
|
async release() {
|
|
62
|
-
resolveExit?.(
|
|
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(
|
|
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<
|
|
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(
|
|
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<
|
|
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:
|
|
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?.(
|
|
110
|
-
triggerIdle: () => resolveIdle?.(
|
|
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(
|
|
113
|
+
throw new Error('Timed out waiting for log follow event');
|
|
128
114
|
}
|
|
129
115
|
|
|
130
|
-
test(
|
|
131
|
-
const fast = makeFakeAgent(
|
|
132
|
-
const slow = makeFakeAgent(
|
|
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,
|
|
136
|
-
assert.equal(result,
|
|
121
|
+
assert.equal(agent.name, 'fast');
|
|
122
|
+
assert.equal(result, 'exited');
|
|
137
123
|
});
|
|
138
124
|
|
|
139
|
-
test(
|
|
140
|
-
const a = makeFakeAgent(
|
|
141
|
-
const b = makeFakeAgent(
|
|
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,
|
|
130
|
+
assert.equal(result, 'timeout');
|
|
145
131
|
});
|
|
146
132
|
|
|
147
|
-
test(
|
|
148
|
-
const agent = makeFakeAgent(
|
|
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,
|
|
155
|
-
assert.equal(result,
|
|
140
|
+
assert.equal(resolved.name, 'releasable');
|
|
141
|
+
assert.equal(result, 'released');
|
|
156
142
|
});
|
|
157
143
|
|
|
158
|
-
test(
|
|
159
|
-
await assert.rejects(
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
168
|
-
const result = await getLogs(
|
|
169
|
-
logsDir:
|
|
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(
|
|
176
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
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(
|
|
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(
|
|
188
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
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 =
|
|
192
|
-
await writeFile(join(dir,
|
|
176
|
+
const logContent = 'line1\nline2\nline3\n';
|
|
177
|
+
await writeFile(join(dir, 'TestAgent.log'), logContent);
|
|
193
178
|
|
|
194
|
-
const result = await getLogs(
|
|
179
|
+
const result = await getLogs('TestAgent', { logsDir: dir, lines: 2 });
|
|
195
180
|
assert.equal(result.found, true);
|
|
196
|
-
assert.equal(result.content,
|
|
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(
|
|
204
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
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,
|
|
208
|
-
await writeFile(join(dir,
|
|
209
|
-
await writeFile(join(dir,
|
|
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(
|
|
213
|
-
assert.ok(agents.includes(
|
|
214
|
-
assert.ok(!agents.includes(
|
|
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(
|
|
221
|
-
const agents = await listLoggedAgents(
|
|
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(
|
|
226
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
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(
|
|
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 ===
|
|
237
|
-
assert.equal(event.type,
|
|
238
|
-
if (event.type ===
|
|
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(
|
|
250
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
251
|
-
const logFile = join(dir,
|
|
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,
|
|
240
|
+
await writeFile(logFile, 'line1\nline2\n');
|
|
256
241
|
|
|
257
|
-
const handle = followLogs(
|
|
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 ===
|
|
265
|
-
assert.equal(historyEvent.type,
|
|
266
|
-
if (historyEvent.type ===
|
|
267
|
-
assert.deepEqual(historyEvent.lines, [
|
|
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,
|
|
255
|
+
await appendFile(logFile, 'line3\nline4\n');
|
|
271
256
|
const deltaEvent = await waitForLogEvent(
|
|
272
257
|
events,
|
|
273
|
-
(item) => item.type ===
|
|
258
|
+
(item) => item.type === 'log' && item.content.includes('line3')
|
|
274
259
|
);
|
|
275
|
-
assert.equal(deltaEvent.type,
|
|
276
|
-
if (deltaEvent.type ===
|
|
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 ===
|
|
266
|
+
const logEventsBeforeStop = events.filter((item) => item.type === 'log').length;
|
|
282
267
|
handle.unsubscribe();
|
|
283
|
-
await appendFile(logFile,
|
|
268
|
+
await appendFile(logFile, 'line5\n');
|
|
284
269
|
await sleep(120);
|
|
285
|
-
const logEventsAfterStop = events.filter((item) => item.type ===
|
|
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(
|
|
293
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
294
|
-
const logFile = join(dir,
|
|
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(
|
|
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 ===
|
|
306
|
-
assert.equal(historyEvent.type,
|
|
307
|
-
if (historyEvent.type ===
|
|
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,
|
|
296
|
+
await writeFile(logFile, 'boot\n');
|
|
312
297
|
const deltaEvent = await waitForLogEvent(
|
|
313
298
|
events,
|
|
314
|
-
(item) => item.type ===
|
|
299
|
+
(item) => item.type === 'log' && item.content.includes('boot')
|
|
315
300
|
);
|
|
316
|
-
assert.equal(deltaEvent.type,
|
|
317
|
-
if (deltaEvent.type ===
|
|
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(
|
|
330
|
-
const { agent, triggerIdle } = makeFakeAgentWithControls(
|
|
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,
|
|
319
|
+
assert.equal(result, 'idle');
|
|
335
320
|
});
|
|
336
321
|
|
|
337
|
-
test(
|
|
338
|
-
const { agent } = makeFakeAgentWithControls(
|
|
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,
|
|
325
|
+
assert.equal(result, 'timeout');
|
|
341
326
|
});
|
|
342
327
|
|
|
343
|
-
test(
|
|
344
|
-
const { agent, triggerExit } = makeFakeAgentWithControls(
|
|
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,
|
|
344
|
+
assert.equal(result, 'timeout');
|
|
360
345
|
});
|
|
361
346
|
|
|
362
|
-
test(
|
|
363
|
-
const { agent } = makeFakeAgentWithControls(
|
|
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,
|
|
350
|
+
assert.equal(result, 'timeout');
|
|
366
351
|
});
|
|
367
352
|
|
|
368
|
-
test(
|
|
369
|
-
const { agent, triggerIdle } = makeFakeAgentWithControls(
|
|
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,
|
|
359
|
+
assert.equal(result, 'idle');
|
|
375
360
|
});
|
|
376
361
|
// ── agent.status ────────────────────────────────────────────────────────────
|
|
377
362
|
|
|
378
|
-
test(
|
|
379
|
-
const { agent } = makeFakeAgentWithControls(
|
|
380
|
-
assert.equal(agent.status,
|
|
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(
|
|
386
|
-
const { agent } = makeFakeAgentWithControls(
|
|
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,
|
|
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(
|
|
397
|
-
const relay = new AgentRelay({ channels: [
|
|
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(
|
|
402
|
-
const relay = new AgentRelay({ channels: [
|
|
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(
|
|
407
|
-
const relay = new AgentRelay({ channels: [
|
|
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 =
|
|
394
|
+
(relay as unknown as { relayApiKey: string }).relayApiKey = 'rk_live_test123';
|
|
410
395
|
const url = relay.observerUrl;
|
|
411
|
-
assert.ok(url,
|
|
396
|
+
assert.ok(url, 'observerUrl should be defined after key is set');
|
|
412
397
|
assert.ok(
|
|
413
|
-
url!.startsWith(
|
|
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(
|
|
417
|
-
assert.equal(relay.workspaceKey,
|
|
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(
|
|
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: [
|
|
428
|
-
env: { RELAY_API_KEY:
|
|
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,
|
|
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://
|
|
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
|
-
})
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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://
|
|
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 =
|
|
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';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/trajectory",
|
|
3
|
-
"version": "3.1.
|
|
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.
|
|
25
|
+
"@agent-relay/config": "3.1.12"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|