@clawroom/openclaw 0.2.1 → 0.2.2
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 +1 -1
- package/package.json +2 -2
- package/src/channel.ts +3 -15
- package/src/client.ts +44 -199
- package/src/task-executor.ts +14 -4
package/README.md
CHANGED
|
@@ -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 HTTP polling.
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawroom/openclaw",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenClaw channel plugin for the Claw Room task marketplace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"openclaw.plugin.json"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@clawroom/sdk": "
|
|
23
|
+
"@clawroom/sdk": "^0.2.2"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"openclaw": "*"
|
package/src/channel.ts
CHANGED
|
@@ -169,10 +169,10 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
169
169
|
});
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
// Wire up
|
|
172
|
+
// Wire up HTTP polling events to OpenClaw health status
|
|
173
173
|
client.onWelcome(() => publishConnected());
|
|
174
174
|
client.onTask(() => {
|
|
175
|
-
// Any server
|
|
175
|
+
// Any server task = update lastEventAt so gateway knows we're alive
|
|
176
176
|
ctx.setStatus({
|
|
177
177
|
accountId: account.accountId,
|
|
178
178
|
running: true,
|
|
@@ -184,18 +184,6 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
186
|
client.onDisconnect(() => publishDisconnected());
|
|
187
|
-
client.onModeChange((mode) => {
|
|
188
|
-
log?.info?.(`[clawroom] mode changed to ${mode}`);
|
|
189
|
-
ctx.setStatus({
|
|
190
|
-
accountId: account.accountId,
|
|
191
|
-
running: true,
|
|
192
|
-
connected: mode === "polling" ? true : client.isConnected,
|
|
193
|
-
lastEventAt: Date.now(),
|
|
194
|
-
lastStartAt: Date.now(),
|
|
195
|
-
lastStopAt: null,
|
|
196
|
-
lastError: mode === "polling" ? "degraded: HTTP polling" : null,
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
187
|
client.onFatal((reason, code) => {
|
|
200
188
|
log?.error?.(`[clawroom] fatal error (code ${code}): ${reason}`);
|
|
201
189
|
ctx.setStatus({
|
|
@@ -212,7 +200,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
212
200
|
client.connect();
|
|
213
201
|
activeClient = client;
|
|
214
202
|
|
|
215
|
-
publishDisconnected("connecting...");
|
|
203
|
+
publishDisconnected("connecting via HTTP polling...");
|
|
216
204
|
|
|
217
205
|
// Health check: if client somehow stopped, restart it.
|
|
218
206
|
const healthCheck = setInterval(() => {
|
package/src/client.ts
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentMessage,
|
|
3
|
-
ServerClaimAck,
|
|
4
|
-
ServerMessage,
|
|
5
|
-
ServerTask,
|
|
6
|
-
} from "@clawroom/sdk";
|
|
7
|
-
|
|
8
|
-
// ── Reconnect policy ─────────────────────────────────────────────────
|
|
9
|
-
const RECONNECT_POLICY = {
|
|
10
|
-
initialMs: 2_000,
|
|
11
|
-
maxMs: 30_000,
|
|
12
|
-
factor: 1.8,
|
|
13
|
-
jitter: 0.25,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
function computeBackoff(attempt: number): number {
|
|
17
|
-
const base = RECONNECT_POLICY.initialMs * RECONNECT_POLICY.factor ** Math.max(attempt - 1, 0);
|
|
18
|
-
const jitter = base * RECONNECT_POLICY.jitter * Math.random();
|
|
19
|
-
return Math.min(RECONNECT_POLICY.maxMs, Math.round(base + jitter));
|
|
20
|
-
}
|
|
1
|
+
import type { AgentMessage, ServerClaimAck, ServerMessage, ServerTask } from "@clawroom/sdk";
|
|
21
2
|
|
|
22
3
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
23
4
|
const POLL_INTERVAL_MS = 10_000;
|
|
@@ -47,23 +28,16 @@ export type ClawroomClientOptions = {
|
|
|
47
28
|
};
|
|
48
29
|
|
|
49
30
|
/**
|
|
50
|
-
* Claw Room agent client using
|
|
31
|
+
* Claw Room agent client using HTTP polling.
|
|
51
32
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Agent→Server actions always use HTTP POST:
|
|
56
|
-
* /api/agents/heartbeat, /complete, /fail, /progress, /claim
|
|
33
|
+
* Agent→Server actions use HTTP POST:
|
|
34
|
+
* /api/agents/heartbeat, /poll, /complete, /fail, /progress, /claim
|
|
57
35
|
*/
|
|
58
36
|
export class ClawroomClient {
|
|
59
37
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
60
38
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
|
-
private reconnectAttempt = 0;
|
|
62
|
-
private consecutiveSseFails = 0;
|
|
63
|
-
private reconnecting = false;
|
|
64
39
|
private stopped = false;
|
|
65
|
-
private
|
|
66
|
-
private pollCycleCount = 0;
|
|
40
|
+
private connected = false;
|
|
67
41
|
private httpBase: string;
|
|
68
42
|
|
|
69
43
|
private taskCallbacks: TaskCallback[] = [];
|
|
@@ -77,9 +51,6 @@ export class ClawroomClient {
|
|
|
77
51
|
private modeChangeCallbacks: ModeChangeCallback[] = [];
|
|
78
52
|
|
|
79
53
|
constructor(private readonly options: ClawroomClientOptions) {
|
|
80
|
-
// Derive HTTP base from endpoint
|
|
81
|
-
// Legacy: wss://host/ws/agent → https://clawroom.site9.ai/api/agents
|
|
82
|
-
// https://clawroom.site9.ai/api/agents/stream → https://clawroom.site9.ai/api/agents
|
|
83
54
|
const ep = options.endpoint;
|
|
84
55
|
if (ep.includes("/api/agents")) {
|
|
85
56
|
this.httpBase = ep.replace(/\/stream\/?$/, "");
|
|
@@ -91,19 +62,16 @@ export class ClawroomClient {
|
|
|
91
62
|
|
|
92
63
|
connect(): void {
|
|
93
64
|
this.stopped = false;
|
|
94
|
-
this.reconnecting = false;
|
|
95
|
-
this.reconnectAttempt = 0;
|
|
96
|
-
this.consecutiveSseFails = 0;
|
|
97
|
-
this.mode = "sse";
|
|
98
|
-
this.doConnectSSE();
|
|
99
65
|
this.startHeartbeat();
|
|
66
|
+
this.startPolling();
|
|
67
|
+
void this.register();
|
|
100
68
|
}
|
|
101
69
|
|
|
102
70
|
disconnect(): void {
|
|
103
71
|
this.stopped = true;
|
|
104
72
|
this.stopHeartbeat();
|
|
105
73
|
this.stopPolling();
|
|
106
|
-
this.
|
|
74
|
+
this.markDisconnected();
|
|
107
75
|
}
|
|
108
76
|
|
|
109
77
|
send(message: AgentMessage): void {
|
|
@@ -113,12 +81,9 @@ export class ClawroomClient {
|
|
|
113
81
|
}
|
|
114
82
|
|
|
115
83
|
get isAlive(): boolean { return !this.stopped; }
|
|
116
|
-
get isConnected(): boolean {
|
|
117
|
-
if (this.mode === "polling") return true;
|
|
118
|
-
return this.sseAbort !== null && !this.sseAbort.signal.aborted;
|
|
119
|
-
}
|
|
84
|
+
get isConnected(): boolean { return this.connected; }
|
|
120
85
|
get isFatal(): boolean { return false; }
|
|
121
|
-
get currentMode(): "sse" | "polling" { return
|
|
86
|
+
get currentMode(): "sse" | "polling" { return "polling"; }
|
|
122
87
|
|
|
123
88
|
onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
|
|
124
89
|
onTaskList(cb: TaskListCallback): void { this.taskListCallbacks.push(cb); }
|
|
@@ -136,7 +101,7 @@ export class ClawroomClient {
|
|
|
136
101
|
this.stopHeartbeat();
|
|
137
102
|
this.heartbeatTimer = setInterval(() => {
|
|
138
103
|
if (this.stopped) return;
|
|
139
|
-
this.
|
|
104
|
+
void this.register();
|
|
140
105
|
}, HEARTBEAT_INTERVAL_MS);
|
|
141
106
|
}
|
|
142
107
|
|
|
@@ -144,147 +109,61 @@ export class ClawroomClient {
|
|
|
144
109
|
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
145
110
|
}
|
|
146
111
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const params = new URLSearchParams({ token, deviceId, skills: skills.join(",") });
|
|
155
|
-
const url = `${this.httpBase}/stream?${params}`;
|
|
156
|
-
|
|
157
|
-
this.options.log?.info?.(`[clawroom] SSE connecting to ${this.httpBase}/stream`);
|
|
158
|
-
|
|
159
|
-
this.sseAbort = new AbortController();
|
|
160
|
-
const signal = this.sseAbort.signal;
|
|
161
|
-
|
|
162
|
-
fetch(url, {
|
|
163
|
-
headers: { "Accept": "text/event-stream", "Authorization": `Bearer ${token}` },
|
|
164
|
-
signal,
|
|
165
|
-
}).then(async (res) => {
|
|
166
|
-
if (!res.ok || !res.body) {
|
|
167
|
-
if (res.status === 401) {
|
|
168
|
-
this.stopped = true;
|
|
169
|
-
this.stopHeartbeat();
|
|
170
|
-
this.stopPolling();
|
|
171
|
-
for (const cb of this.fatalCallbacks) cb("Unauthorized");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
throw new Error(`HTTP ${res.status}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
this.options.log?.info?.("[clawroom] SSE connected");
|
|
178
|
-
this.reconnectAttempt = 0;
|
|
179
|
-
this.consecutiveSseFails = 0;
|
|
180
|
-
if (this.mode === "polling") this.switchToSSE();
|
|
181
|
-
|
|
182
|
-
const reader = res.body.getReader();
|
|
183
|
-
const decoder = new TextDecoder();
|
|
184
|
-
let buffer = "";
|
|
185
|
-
|
|
186
|
-
while (true) {
|
|
187
|
-
const { done, value } = await reader.read();
|
|
188
|
-
if (done) break;
|
|
189
|
-
|
|
190
|
-
buffer += decoder.decode(value, { stream: true });
|
|
191
|
-
const lines = buffer.split("\n");
|
|
192
|
-
buffer = lines.pop() ?? "";
|
|
193
|
-
|
|
194
|
-
for (const line of lines) {
|
|
195
|
-
if (line.startsWith("data: ")) {
|
|
196
|
-
this.handleMessage(line.slice(6));
|
|
197
|
-
}
|
|
198
|
-
// Ignore comments (: keepalive) and empty lines
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Stream ended
|
|
203
|
-
this.options.log?.info?.("[clawroom] SSE stream ended");
|
|
204
|
-
for (const cb of this.disconnectCallbacks) cb();
|
|
205
|
-
this.triggerReconnect("stream ended");
|
|
206
|
-
}).catch((err) => {
|
|
207
|
-
if (signal.aborted) return;
|
|
208
|
-
this.options.log?.warn?.(`[clawroom] SSE error: ${err}`);
|
|
209
|
-
for (const cb of this.disconnectCallbacks) cb();
|
|
210
|
-
this.triggerReconnect("SSE error");
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private destroySSE(): void {
|
|
215
|
-
if (this.sseAbort) {
|
|
216
|
-
try { this.sseAbort.abort(); } catch {}
|
|
217
|
-
this.sseAbort = null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private triggerReconnect(reason: string): void {
|
|
222
|
-
if (this.reconnecting || this.stopped) return;
|
|
223
|
-
|
|
224
|
-
this.consecutiveSseFails++;
|
|
225
|
-
|
|
226
|
-
// SSE keeps failing → degrade to polling
|
|
227
|
-
if (this.consecutiveSseFails >= 3 && this.mode !== "polling") {
|
|
228
|
-
this.options.log?.warn?.(`[clawroom] SSE failed ${this.consecutiveSseFails} times, switching to HTTP polling`);
|
|
229
|
-
this.switchToPolling();
|
|
230
|
-
return;
|
|
112
|
+
private markConnected(agentId?: string): void {
|
|
113
|
+
if (this.connected) return;
|
|
114
|
+
this.connected = true;
|
|
115
|
+
this.options.log?.info?.("[clawroom] polling connected");
|
|
116
|
+
for (const cb of this.modeChangeCallbacks) cb("polling");
|
|
117
|
+
if (agentId) {
|
|
118
|
+
for (const cb of this.welcomeCallbacks) cb(agentId);
|
|
231
119
|
}
|
|
232
|
-
|
|
233
|
-
this.reconnecting = true;
|
|
234
|
-
const delayMs = computeBackoff(this.reconnectAttempt);
|
|
235
|
-
this.reconnectAttempt++;
|
|
236
|
-
this.options.log?.info?.(`[clawroom] reconnecting in ${delayMs}ms (${reason}, attempt ${this.reconnectAttempt})`);
|
|
237
|
-
|
|
238
|
-
setTimeout(() => {
|
|
239
|
-
this.reconnecting = false;
|
|
240
|
-
if (!this.stopped) this.doConnectSSE();
|
|
241
|
-
}, delayMs);
|
|
242
120
|
}
|
|
243
121
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.
|
|
248
|
-
this.destroySSE();
|
|
249
|
-
this.options.log?.info?.("[clawroom] entering HTTP polling mode");
|
|
250
|
-
for (const cb of this.modeChangeCallbacks) cb("polling");
|
|
251
|
-
|
|
252
|
-
this.pollCycleCount = 0;
|
|
253
|
-
this.stopPolling();
|
|
254
|
-
this.pollTimer = setInterval(() => this.pollTick(), POLL_INTERVAL_MS);
|
|
122
|
+
private markDisconnected(): void {
|
|
123
|
+
if (!this.connected) return;
|
|
124
|
+
this.connected = false;
|
|
125
|
+
for (const cb of this.disconnectCallbacks) cb();
|
|
255
126
|
}
|
|
256
127
|
|
|
257
|
-
private
|
|
258
|
-
this.options.log?.info?.("[clawroom] SSE restored, switching back from polling");
|
|
128
|
+
private startPolling(): void {
|
|
259
129
|
this.stopPolling();
|
|
260
|
-
this.
|
|
261
|
-
|
|
130
|
+
this.options.log?.info?.(`[clawroom] polling ${this.httpBase}/poll`);
|
|
131
|
+
this.pollTimer = setInterval(() => {
|
|
132
|
+
void this.pollTick();
|
|
133
|
+
}, POLL_INTERVAL_MS);
|
|
262
134
|
}
|
|
263
135
|
|
|
264
136
|
private stopPolling(): void {
|
|
265
137
|
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
266
138
|
}
|
|
267
139
|
|
|
140
|
+
private async register(): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
const res = await this.httpRequest("POST", "/heartbeat", {
|
|
143
|
+
deviceId: this.options.deviceId,
|
|
144
|
+
skills: this.options.skills,
|
|
145
|
+
});
|
|
146
|
+
this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
268
152
|
private async pollTick(): Promise<void> {
|
|
269
153
|
if (this.stopped) return;
|
|
270
|
-
this.pollCycleCount++;
|
|
271
154
|
|
|
272
155
|
try {
|
|
273
156
|
const res = await this.httpRequest("POST", "/poll", {});
|
|
157
|
+
this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
|
|
274
158
|
if (res.task) {
|
|
275
159
|
const task = res.task as ServerTask;
|
|
276
|
-
this.options.log?.info?.(`[clawroom]
|
|
160
|
+
this.options.log?.info?.(`[clawroom] received task ${task.taskId}: ${task.title}`);
|
|
277
161
|
for (const cb of this.taskCallbacks) cb(task);
|
|
278
162
|
for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: task.taskId, ok: true });
|
|
279
163
|
}
|
|
280
164
|
} catch (err) {
|
|
281
|
-
this.options.log?.warn?.(`[clawroom]
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Try to restore SSE every 60s
|
|
285
|
-
if (this.pollCycleCount % 6 === 0) {
|
|
286
|
-
this.options.log?.info?.("[clawroom] [poll] attempting SSE restore...");
|
|
287
|
-
this.doConnectSSE();
|
|
165
|
+
this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
|
|
166
|
+
this.markDisconnected();
|
|
288
167
|
}
|
|
289
168
|
}
|
|
290
169
|
|
|
@@ -338,45 +217,11 @@ export class ClawroomClient {
|
|
|
338
217
|
this.stopped = true;
|
|
339
218
|
this.stopHeartbeat();
|
|
340
219
|
this.stopPolling();
|
|
341
|
-
this.
|
|
220
|
+
this.markDisconnected();
|
|
342
221
|
for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`);
|
|
343
222
|
}
|
|
344
223
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
345
224
|
}
|
|
346
225
|
return res.json();
|
|
347
226
|
}
|
|
348
|
-
|
|
349
|
-
// ── Message handling ──────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
private handleMessage(raw: string): void {
|
|
352
|
-
let msg: ServerMessage;
|
|
353
|
-
try { msg = JSON.parse(raw) as ServerMessage; } catch { return; }
|
|
354
|
-
|
|
355
|
-
switch (msg.type) {
|
|
356
|
-
case "server.welcome":
|
|
357
|
-
this.options.log?.info?.(`[clawroom] welcome, agentId=${msg.agentId}`);
|
|
358
|
-
for (const cb of this.welcomeCallbacks) cb(msg.agentId);
|
|
359
|
-
break;
|
|
360
|
-
case "server.pong":
|
|
361
|
-
break;
|
|
362
|
-
case "server.task":
|
|
363
|
-
this.options.log?.info?.(`[clawroom] received task ${msg.taskId}: ${msg.title}`);
|
|
364
|
-
for (const cb of this.taskCallbacks) cb(msg);
|
|
365
|
-
break;
|
|
366
|
-
case "server.task_list":
|
|
367
|
-
this.options.log?.info?.(`[clawroom] received ${msg.tasks.length} open task(s)`);
|
|
368
|
-
for (const cb of this.taskListCallbacks) cb(msg.tasks);
|
|
369
|
-
break;
|
|
370
|
-
case "server.claim_ack":
|
|
371
|
-
this.options.log?.info?.(`[clawroom] claim_ack taskId=${msg.taskId} ok=${msg.ok}${msg.reason ? ` reason=${msg.reason}` : ""}`);
|
|
372
|
-
for (const cb of this.claimAckCallbacks) cb(msg);
|
|
373
|
-
break;
|
|
374
|
-
case "server.error":
|
|
375
|
-
this.options.log?.error?.(`[clawroom] server error: ${msg.message}`);
|
|
376
|
-
for (const cb of this.errorCallbacks) cb(msg);
|
|
377
|
-
break;
|
|
378
|
-
default:
|
|
379
|
-
this.options.log?.warn?.("[clawroom] unknown message type", msg);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
227
|
}
|
package/src/task-executor.ts
CHANGED
|
@@ -12,10 +12,11 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Wire up the task execution pipeline:
|
|
15
|
-
* 1. On server.task / server.task_list -> store as available
|
|
15
|
+
* 1. On server.task / server.task_list -> store as available
|
|
16
16
|
* 2. On claim_ack(ok) -> invoke subagent -> send result/fail
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Tasks arrive through the polling client, either because the agent auto-claimed
|
|
19
|
+
* them or because the owner manually claimed them from the dashboard.
|
|
19
20
|
*/
|
|
20
21
|
export function setupTaskExecutor(opts: {
|
|
21
22
|
client: ClawroomClient;
|
|
@@ -28,8 +29,9 @@ export function setupTaskExecutor(opts: {
|
|
|
28
29
|
}): void {
|
|
29
30
|
const { client, runtime, log } = opts;
|
|
30
31
|
|
|
31
|
-
// Track received tasks
|
|
32
|
+
// Track received tasks until they transition into an active execution.
|
|
32
33
|
const knownTasks = new Map<string, ServerTask>();
|
|
34
|
+
const activeTasks = new Set<string>();
|
|
33
35
|
|
|
34
36
|
// New task received — store it for potential execution
|
|
35
37
|
client.onTask((task: ServerTask) => {
|
|
@@ -59,6 +61,11 @@ export function setupTaskExecutor(opts: {
|
|
|
59
61
|
const task = knownTasks.get(ack.taskId);
|
|
60
62
|
knownTasks.delete(ack.taskId);
|
|
61
63
|
|
|
64
|
+
if (activeTasks.has(ack.taskId)) {
|
|
65
|
+
log?.info?.(`[clawroom:executor] task ${ack.taskId} is already running, ignoring duplicate dispatch`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
if (!task) {
|
|
63
70
|
log?.warn?.(
|
|
64
71
|
`[clawroom:executor] claim_ack for unknown task ${ack.taskId}`,
|
|
@@ -67,7 +74,10 @@ export function setupTaskExecutor(opts: {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
log?.info?.(`[clawroom:executor] executing task ${task.taskId}`);
|
|
70
|
-
|
|
77
|
+
activeTasks.add(task.taskId);
|
|
78
|
+
void executeTask({ client, runtime, task, log }).finally(() => {
|
|
79
|
+
activeTasks.delete(task.taskId);
|
|
80
|
+
});
|
|
71
81
|
});
|
|
72
82
|
}
|
|
73
83
|
|