@clawroom/openclaw 0.1.1 → 0.2.0
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 +2 -2
- package/package.json +1 -1
- package/src/channel.ts +1 -1
- package/src/ws-client.ts +114 -177
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ openclaw gateway restart
|
|
|
15
15
|
| Key | Description |
|
|
16
16
|
|---|---|
|
|
17
17
|
| `channels.clawroom.token` | Lobster token from the ClawRoom dashboard |
|
|
18
|
-
| `channels.clawroom.endpoint` |
|
|
18
|
+
| `channels.clawroom.endpoint` | API endpoint (default: `https://clawroom.site9.ai/api/agents`) |
|
|
19
19
|
| `channels.clawroom.skills` | Optional array of skill tags to advertise |
|
|
20
20
|
| `channels.clawroom.enabled` | Enable/disable the channel (default: `true`) |
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ openclaw gateway restart
|
|
|
23
23
|
|
|
24
24
|
1. Sign up at ClawRoom and create a lobster token in the dashboard.
|
|
25
25
|
2. Install this plugin and configure the token.
|
|
26
|
-
3. Restart your gateway. The plugin connects to ClawRoom via
|
|
26
|
+
3. Restart your gateway. The plugin connects to ClawRoom via SSE + HTTP.
|
|
27
27
|
4. Claim tasks from the dashboard. Your lobster executes them using OpenClaw's subagent runtime and reports results back automatically.
|
|
28
28
|
|
|
29
29
|
## Release
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { setupTaskExecutor } from "./task-executor.js";
|
|
|
7
7
|
|
|
8
8
|
// ── Config resolution ────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
const DEFAULT_ENDPOINT = "
|
|
10
|
+
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
|
|
11
11
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
12
12
|
|
|
13
13
|
interface ClawroomAccountConfig {
|
package/src/ws-client.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ServerTask,
|
|
6
6
|
} from "@clawroom/sdk";
|
|
7
7
|
|
|
8
|
-
// ── Reconnect policy
|
|
8
|
+
// ── Reconnect policy ─────────────────────────────────────────────────
|
|
9
9
|
const RECONNECT_POLICY = {
|
|
10
10
|
initialMs: 2_000,
|
|
11
11
|
maxMs: 30_000,
|
|
@@ -19,22 +19,8 @@ function computeBackoff(attempt: number): number {
|
|
|
19
19
|
return Math.min(RECONNECT_POLICY.maxMs, Math.round(base + jitter));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
const NON_RECOVERABLE_CODES = new Set([
|
|
24
|
-
4001, // Missing token
|
|
25
|
-
4002, // Invalid token
|
|
26
|
-
4003, // Token revoked / agent deleted
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const WATCHDOG_INTERVAL_MS = 10_000;
|
|
30
|
-
const DEAD_THRESHOLD_MS = 25_000;
|
|
31
|
-
|
|
32
|
-
// WS fails this many times in a row → switch to HTTP polling
|
|
33
|
-
const WS_FAIL_THRESHOLD = 5;
|
|
34
|
-
// HTTP poll interval when in degraded mode
|
|
22
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
35
23
|
const POLL_INTERVAL_MS = 10_000;
|
|
36
|
-
// Try to restore WS every N poll cycles
|
|
37
|
-
const WS_RESTORE_INTERVAL = 6; // = 60s
|
|
38
24
|
|
|
39
25
|
// ── Types ─────────────────────────────────────────────────────────────
|
|
40
26
|
|
|
@@ -45,8 +31,8 @@ type ClaimRequestCallback = (task: ServerTask) => void;
|
|
|
45
31
|
type ErrorCallback = (error: ServerMessage & { type: "server.error" }) => void;
|
|
46
32
|
type DisconnectCallback = () => void;
|
|
47
33
|
type WelcomeCallback = (agentId: string) => void;
|
|
48
|
-
type FatalCallback = (reason: string
|
|
49
|
-
type ModeChangeCallback = (mode: "
|
|
34
|
+
type FatalCallback = (reason: string) => void;
|
|
35
|
+
type ModeChangeCallback = (mode: "sse" | "polling") => void;
|
|
50
36
|
|
|
51
37
|
export type ClawroomWsClientOptions = {
|
|
52
38
|
endpoint: string;
|
|
@@ -61,29 +47,25 @@ export type ClawroomWsClientOptions = {
|
|
|
61
47
|
};
|
|
62
48
|
|
|
63
49
|
/**
|
|
64
|
-
*
|
|
50
|
+
* Claw Room agent client using SSE (server push) + HTTP (agent actions).
|
|
65
51
|
*
|
|
66
|
-
* Primary mode:
|
|
67
|
-
* Fallback mode: HTTP
|
|
52
|
+
* Primary mode: SSE stream at /api/agents/stream (real-time task push)
|
|
53
|
+
* Fallback mode: HTTP polling at /api/agents/poll (when SSE fails 3+ times)
|
|
68
54
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* - While polling, try to restore WS every 60s
|
|
72
|
-
* - WS reconnects successfully → switch back to WS, stop polling
|
|
55
|
+
* Agent→Server actions always use HTTP POST:
|
|
56
|
+
* /api/agents/heartbeat, /complete, /fail, /progress, /claim
|
|
73
57
|
*/
|
|
74
58
|
export class ClawroomWsClient {
|
|
75
|
-
private
|
|
76
|
-
private
|
|
59
|
+
private eventSource: EventSource | null = null;
|
|
60
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
77
61
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
78
|
-
private lastActivity = 0;
|
|
79
62
|
private reconnectAttempt = 0;
|
|
80
|
-
private
|
|
63
|
+
private consecutiveSseFails = 0;
|
|
81
64
|
private reconnecting = false;
|
|
82
65
|
private stopped = false;
|
|
83
|
-
private
|
|
84
|
-
private mode: "ws" | "polling" = "ws";
|
|
66
|
+
private mode: "sse" | "polling" = "sse";
|
|
85
67
|
private pollCycleCount = 0;
|
|
86
|
-
private httpBase
|
|
68
|
+
private httpBase: string;
|
|
87
69
|
|
|
88
70
|
private taskCallbacks: TaskCallback[] = [];
|
|
89
71
|
private taskListCallbacks: TaskListCallback[] = [];
|
|
@@ -96,50 +78,48 @@ export class ClawroomWsClient {
|
|
|
96
78
|
private modeChangeCallbacks: ModeChangeCallback[] = [];
|
|
97
79
|
|
|
98
80
|
constructor(private readonly options: ClawroomWsClientOptions) {
|
|
99
|
-
// Derive HTTP base
|
|
81
|
+
// Derive HTTP base from endpoint
|
|
100
82
|
// wss://clawroom.site9.ai/ws/agent → https://clawroom.site9.ai/api/agents
|
|
101
|
-
//
|
|
83
|
+
// https://clawroom.site9.ai/api/agents/stream → https://clawroom.site9.ai/api/agents
|
|
102
84
|
const ep = options.endpoint;
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
if (ep.includes("/api/agents")) {
|
|
86
|
+
this.httpBase = ep.replace(/\/stream\/?$/, "");
|
|
87
|
+
} else {
|
|
88
|
+
const httpUrl = ep.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
89
|
+
this.httpBase = httpUrl.replace(/\/ws\/.*$/, "/api/agents");
|
|
90
|
+
}
|
|
105
91
|
}
|
|
106
92
|
|
|
107
93
|
connect(): void {
|
|
108
94
|
this.stopped = false;
|
|
109
95
|
this.reconnecting = false;
|
|
110
96
|
this.reconnectAttempt = 0;
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
this.doConnect();
|
|
116
|
-
this.startWatchdog();
|
|
97
|
+
this.consecutiveSseFails = 0;
|
|
98
|
+
this.mode = "sse";
|
|
99
|
+
this.doConnectSSE();
|
|
100
|
+
this.startHeartbeat();
|
|
117
101
|
}
|
|
118
102
|
|
|
119
103
|
disconnect(): void {
|
|
120
104
|
this.stopped = true;
|
|
121
|
-
this.
|
|
105
|
+
this.stopHeartbeat();
|
|
122
106
|
this.stopPolling();
|
|
123
|
-
this.
|
|
107
|
+
this.destroySSE();
|
|
124
108
|
}
|
|
125
109
|
|
|
126
110
|
send(message: AgentMessage): void {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
133
|
-
try { this.ws.send(JSON.stringify(message)); } catch {}
|
|
111
|
+
this.sendViaHttp(message).catch((err) => {
|
|
112
|
+
this.options.log?.warn?.(`[clawroom] send error: ${err}`);
|
|
113
|
+
});
|
|
134
114
|
}
|
|
135
115
|
|
|
136
116
|
get isAlive(): boolean { return !this.stopped; }
|
|
137
117
|
get isConnected(): boolean {
|
|
138
|
-
if (this.mode === "polling") return true;
|
|
139
|
-
return this.
|
|
118
|
+
if (this.mode === "polling") return true;
|
|
119
|
+
return this.eventSource?.readyState === EventSource.OPEN;
|
|
140
120
|
}
|
|
141
|
-
get isFatal(): boolean { return
|
|
142
|
-
get currentMode(): "
|
|
121
|
+
get isFatal(): boolean { return false; }
|
|
122
|
+
get currentMode(): "sse" | "polling" { return this.mode; }
|
|
143
123
|
|
|
144
124
|
onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
|
|
145
125
|
onTaskList(cb: TaskListCallback): void { this.taskListCallbacks.push(cb); }
|
|
@@ -151,156 +131,111 @@ export class ClawroomWsClient {
|
|
|
151
131
|
onFatal(cb: FatalCallback): void { this.fatalCallbacks.push(cb); }
|
|
152
132
|
onModeChange(cb: ModeChangeCallback): void { this.modeChangeCallbacks.push(cb); }
|
|
153
133
|
|
|
154
|
-
// ──
|
|
134
|
+
// ── Heartbeat (HTTP POST, both modes) ───────────────────────────
|
|
155
135
|
|
|
156
|
-
private
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
136
|
+
private startHeartbeat(): void {
|
|
137
|
+
this.stopHeartbeat();
|
|
138
|
+
this.heartbeatTimer = setInterval(() => {
|
|
139
|
+
if (this.stopped) return;
|
|
140
|
+
this.httpRequest("POST", "/heartbeat", {}).catch(() => {});
|
|
141
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
159
142
|
}
|
|
160
143
|
|
|
161
|
-
private
|
|
162
|
-
if (this.
|
|
144
|
+
private stopHeartbeat(): void {
|
|
145
|
+
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
163
146
|
}
|
|
164
147
|
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
// ── SSE Connection ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
private doConnectSSE(): void {
|
|
151
|
+
this.destroySSE();
|
|
152
|
+
const { token, deviceId, skills } = this.options;
|
|
153
|
+
const params = new URLSearchParams({
|
|
154
|
+
token,
|
|
155
|
+
deviceId,
|
|
156
|
+
skills: skills.join(","),
|
|
157
|
+
});
|
|
158
|
+
const url = `${this.httpBase}/stream?${params}`;
|
|
167
159
|
|
|
168
|
-
|
|
160
|
+
this.options.log?.info?.(`[clawroom] SSE connecting to ${this.httpBase}/stream`);
|
|
169
161
|
|
|
170
|
-
|
|
171
|
-
this.
|
|
162
|
+
try {
|
|
163
|
+
this.eventSource = new EventSource(url);
|
|
164
|
+
} catch {
|
|
165
|
+
this.triggerReconnect("EventSource creation failed");
|
|
172
166
|
return;
|
|
173
167
|
}
|
|
174
168
|
|
|
175
|
-
|
|
169
|
+
this.eventSource.onopen = () => {
|
|
170
|
+
this.options.log?.info?.("[clawroom] SSE connected");
|
|
171
|
+
this.reconnectAttempt = 0;
|
|
172
|
+
this.consecutiveSseFails = 0;
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
174
|
+
if (this.mode === "polling") {
|
|
175
|
+
this.switchToSSE();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
183
178
|
|
|
184
|
-
this.
|
|
179
|
+
this.eventSource.onmessage = (event) => {
|
|
180
|
+
this.handleMessage(event.data);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
this.eventSource.onerror = () => {
|
|
184
|
+
this.options.log?.info?.("[clawroom] SSE error/disconnected");
|
|
185
|
+
this.destroySSE();
|
|
186
|
+
for (const cb of this.disconnectCallbacks) cb();
|
|
187
|
+
this.triggerReconnect("SSE error");
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private destroySSE(): void {
|
|
192
|
+
if (this.eventSource) {
|
|
193
|
+
try { this.eventSource.close(); } catch {}
|
|
194
|
+
this.eventSource = null;
|
|
195
|
+
}
|
|
185
196
|
}
|
|
186
197
|
|
|
187
198
|
private triggerReconnect(reason: string): void {
|
|
188
199
|
if (this.reconnecting || this.stopped) return;
|
|
189
200
|
|
|
190
|
-
|
|
191
|
-
if (NON_RECOVERABLE_CODES.has(this.lastCloseCode)) {
|
|
192
|
-
const msg = `non-recoverable close code ${this.lastCloseCode}, giving up`;
|
|
193
|
-
this.options.log?.error?.(`[clawroom] ${msg}`);
|
|
194
|
-
this.stopped = true;
|
|
195
|
-
this.stopWatchdog();
|
|
196
|
-
this.stopPolling();
|
|
197
|
-
for (const cb of this.fatalCallbacks) cb(msg, this.lastCloseCode);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.consecutiveWsFails++;
|
|
201
|
+
this.consecutiveSseFails++;
|
|
202
202
|
|
|
203
|
-
//
|
|
204
|
-
if (this.
|
|
205
|
-
this.options.log?.warn?.(`[clawroom]
|
|
203
|
+
// SSE keeps failing → degrade to polling
|
|
204
|
+
if (this.consecutiveSseFails >= 3 && this.mode !== "polling") {
|
|
205
|
+
this.options.log?.warn?.(`[clawroom] SSE failed ${this.consecutiveSseFails} times, switching to HTTP polling`);
|
|
206
206
|
this.switchToPolling();
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
this.reconnecting = true;
|
|
211
|
-
this.destroySocket();
|
|
212
|
-
|
|
213
211
|
const delayMs = computeBackoff(this.reconnectAttempt);
|
|
214
212
|
this.reconnectAttempt++;
|
|
215
213
|
this.options.log?.info?.(`[clawroom] reconnecting in ${delayMs}ms (${reason}, attempt ${this.reconnectAttempt})`);
|
|
216
214
|
|
|
217
215
|
setTimeout(() => {
|
|
218
216
|
this.reconnecting = false;
|
|
219
|
-
if (!this.stopped) this.
|
|
217
|
+
if (!this.stopped) this.doConnectSSE();
|
|
220
218
|
}, delayMs);
|
|
221
219
|
}
|
|
222
220
|
|
|
223
|
-
// ──
|
|
224
|
-
|
|
225
|
-
private doConnect(): void {
|
|
226
|
-
this.destroySocket();
|
|
227
|
-
const { endpoint, token } = this.options;
|
|
228
|
-
const url = `${endpoint}?token=${encodeURIComponent(token)}`;
|
|
229
|
-
|
|
230
|
-
this.options.log?.info?.(`[clawroom] connecting to ${endpoint}`);
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
this.ws = new WebSocket(url);
|
|
234
|
-
} catch {
|
|
235
|
-
return; // watchdog will retry
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
this.ws.addEventListener("open", () => {
|
|
239
|
-
this.options.log?.info?.("[clawroom] WS connected");
|
|
240
|
-
this.reconnectAttempt = 0;
|
|
241
|
-
this.consecutiveWsFails = 0;
|
|
242
|
-
this.lastCloseCode = 0;
|
|
243
|
-
this.lastActivity = Date.now();
|
|
244
|
-
|
|
245
|
-
// If we were polling, switch back to WS
|
|
246
|
-
if (this.mode === "polling") {
|
|
247
|
-
this.switchToWs();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
this.ws?.send(JSON.stringify({
|
|
251
|
-
type: "agent.hello",
|
|
252
|
-
deviceId: this.options.deviceId,
|
|
253
|
-
skills: this.options.skills,
|
|
254
|
-
}));
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
this.ws.addEventListener("message", (event) => {
|
|
258
|
-
this.lastActivity = Date.now();
|
|
259
|
-
this.handleMessage(event.data as string);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
this.ws.addEventListener("close", (event) => {
|
|
263
|
-
this.lastCloseCode = event.code;
|
|
264
|
-
const reason = event.reason || `code ${event.code}`;
|
|
265
|
-
this.options.log?.info?.(`[clawroom] WS disconnected (${reason})`);
|
|
266
|
-
this.ws = null;
|
|
267
|
-
for (const cb of this.disconnectCallbacks) cb();
|
|
268
|
-
if (NON_RECOVERABLE_CODES.has(event.code)) {
|
|
269
|
-
this.triggerReconnect(reason);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
this.ws.addEventListener("error", () => {});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private destroySocket(): void {
|
|
277
|
-
if (this.ws) {
|
|
278
|
-
try { this.ws.close(); } catch {}
|
|
279
|
-
this.ws = null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── HTTP Polling (fallback mode) ────────────────────────────────
|
|
221
|
+
// ── Polling fallback ────────────────────────────────────────────
|
|
284
222
|
|
|
285
223
|
private switchToPolling(): void {
|
|
286
224
|
this.mode = "polling";
|
|
287
|
-
this.
|
|
225
|
+
this.destroySSE();
|
|
288
226
|
this.options.log?.info?.("[clawroom] entering HTTP polling mode");
|
|
289
227
|
for (const cb of this.modeChangeCallbacks) cb("polling");
|
|
290
228
|
|
|
291
|
-
// Send initial heartbeat
|
|
292
|
-
this.httpHeartbeat().catch(() => {});
|
|
293
|
-
|
|
294
229
|
this.pollCycleCount = 0;
|
|
295
230
|
this.stopPolling();
|
|
296
231
|
this.pollTimer = setInterval(() => this.pollTick(), POLL_INTERVAL_MS);
|
|
297
232
|
}
|
|
298
233
|
|
|
299
|
-
private
|
|
300
|
-
this.options.log?.info?.("[clawroom]
|
|
234
|
+
private switchToSSE(): void {
|
|
235
|
+
this.options.log?.info?.("[clawroom] SSE restored, switching back from polling");
|
|
301
236
|
this.stopPolling();
|
|
302
|
-
this.mode = "
|
|
303
|
-
for (const cb of this.modeChangeCallbacks) cb("
|
|
237
|
+
this.mode = "sse";
|
|
238
|
+
for (const cb of this.modeChangeCallbacks) cb("sse");
|
|
304
239
|
}
|
|
305
240
|
|
|
306
241
|
private stopPolling(): void {
|
|
@@ -309,20 +244,13 @@ export class ClawroomWsClient {
|
|
|
309
244
|
|
|
310
245
|
private async pollTick(): Promise<void> {
|
|
311
246
|
if (this.stopped) return;
|
|
312
|
-
|
|
313
247
|
this.pollCycleCount++;
|
|
314
248
|
|
|
315
|
-
// Heartbeat
|
|
316
|
-
await this.httpHeartbeat().catch(() => {});
|
|
317
|
-
|
|
318
|
-
// Poll for tasks — task-executor handles execution via callbacks
|
|
319
|
-
// (poll endpoint auto-claims, so we just need to trigger task callbacks)
|
|
320
249
|
try {
|
|
321
250
|
const res = await this.httpRequest("POST", "/poll", {});
|
|
322
251
|
if (res.task) {
|
|
323
252
|
const task = res.task as ServerTask;
|
|
324
253
|
this.options.log?.info?.(`[clawroom] [poll] received task ${task.taskId}: ${task.title}`);
|
|
325
|
-
// Fire task + claim_ack callbacks so task-executor picks it up
|
|
326
254
|
for (const cb of this.taskCallbacks) cb(task);
|
|
327
255
|
for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: task.taskId, ok: true });
|
|
328
256
|
}
|
|
@@ -330,16 +258,14 @@ export class ClawroomWsClient {
|
|
|
330
258
|
this.options.log?.warn?.(`[clawroom] [poll] error: ${err}`);
|
|
331
259
|
}
|
|
332
260
|
|
|
333
|
-
//
|
|
334
|
-
if (this.pollCycleCount %
|
|
335
|
-
this.options.log?.info?.("[clawroom] [poll] attempting
|
|
336
|
-
this.
|
|
261
|
+
// Try to restore SSE every 60s
|
|
262
|
+
if (this.pollCycleCount % 6 === 0) {
|
|
263
|
+
this.options.log?.info?.("[clawroom] [poll] attempting SSE restore...");
|
|
264
|
+
this.doConnectSSE();
|
|
337
265
|
}
|
|
338
266
|
}
|
|
339
267
|
|
|
340
|
-
|
|
341
|
-
await this.httpRequest("POST", "/heartbeat", {}).catch(() => {});
|
|
342
|
-
}
|
|
268
|
+
// ── HTTP send ───────────────────────────────────────────────────
|
|
343
269
|
|
|
344
270
|
private async sendViaHttp(message: AgentMessage): Promise<void> {
|
|
345
271
|
switch (message.type) {
|
|
@@ -364,9 +290,13 @@ export class ClawroomWsClient {
|
|
|
364
290
|
});
|
|
365
291
|
break;
|
|
366
292
|
case "agent.heartbeat":
|
|
367
|
-
await this.
|
|
293
|
+
await this.httpRequest("POST", "/heartbeat", {});
|
|
294
|
+
break;
|
|
295
|
+
case "agent.claim":
|
|
296
|
+
await this.httpRequest("POST", "/claim", { taskId: message.taskId });
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
368
299
|
break;
|
|
369
|
-
// claim, hello, release — not needed in polling mode
|
|
370
300
|
}
|
|
371
301
|
}
|
|
372
302
|
|
|
@@ -381,6 +311,13 @@ export class ClawroomWsClient {
|
|
|
381
311
|
});
|
|
382
312
|
if (!res.ok) {
|
|
383
313
|
const text = await res.text().catch(() => "");
|
|
314
|
+
if (res.status === 401) {
|
|
315
|
+
this.stopped = true;
|
|
316
|
+
this.stopHeartbeat();
|
|
317
|
+
this.stopPolling();
|
|
318
|
+
this.destroySSE();
|
|
319
|
+
for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`);
|
|
320
|
+
}
|
|
384
321
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
385
322
|
}
|
|
386
323
|
return res.json();
|