@clawroom/openclaw 0.2.1 → 0.2.3
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 +7 -7
- package/package.json +3 -3
- package/src/channel.ts +4 -16
- package/src/client.ts +49 -224
- package/src/task-executor.ts +25 -21
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClawRoom
|
|
2
2
|
|
|
3
3
|
OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenClaw gateway to ClawRoom so your lobsters can claim and execute tasks.
|
|
4
4
|
|
|
@@ -23,16 +23,16 @@ 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
|
|
30
30
|
|
|
31
|
-
The repository includes a GitHub Actions workflow that publishes
|
|
31
|
+
The repository includes a GitHub Actions workflow that publishes `@clawroom/protocol`, `@clawroom/sdk`, and `@clawroom/openclaw` to npm when a release tag is pushed.
|
|
32
32
|
|
|
33
33
|
To publish a new version:
|
|
34
34
|
|
|
35
|
-
1. Update `
|
|
36
|
-
2.
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
1. Update the package versions in `protocol/package.json`, `sdk/package.json`, and `plugin/package.json`.
|
|
36
|
+
2. Commit and push the release commit to GitHub.
|
|
37
|
+
3. Push a release tag, for example `git tag plugin-0.2.3 && git push origin plugin-0.2.3`.
|
|
38
|
+
4. Watch `.github/workflows/release-plugin.yml` until all three publish jobs succeed.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawroom/openclaw",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "OpenClaw channel plugin for the
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "OpenClaw channel plugin for the ClawRoom task marketplace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"openclaw.plugin.json"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@clawroom/sdk": "
|
|
23
|
+
"@clawroom/sdk": "^0.2.3"
|
|
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(() => {
|
|
@@ -280,7 +268,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
280
268
|
* runtime environment when available, falling back to a random id.
|
|
281
269
|
*/
|
|
282
270
|
function resolveDeviceId(ctx: {
|
|
283
|
-
runtime
|
|
271
|
+
runtime?: unknown;
|
|
284
272
|
}): string {
|
|
285
273
|
// The RuntimeEnv may expose hostname or machineId depending on version
|
|
286
274
|
const r = ctx.runtime as Record<string, unknown>;
|
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, ServerTask } from "@clawroom/sdk";
|
|
21
2
|
|
|
22
3
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
23
4
|
const POLL_INTERVAL_MS = 10_000;
|
|
@@ -25,14 +6,10 @@ const POLL_INTERVAL_MS = 10_000;
|
|
|
25
6
|
// ── Types ─────────────────────────────────────────────────────────────
|
|
26
7
|
|
|
27
8
|
type TaskCallback = (task: ServerTask) => void;
|
|
28
|
-
type TaskListCallback = (tasks: ServerTask[]) => void;
|
|
29
9
|
type ClaimAckCallback = (ack: ServerClaimAck) => void;
|
|
30
|
-
type ClaimRequestCallback = (task: ServerTask) => void;
|
|
31
|
-
type ErrorCallback = (error: ServerMessage & { type: "server.error" }) => void;
|
|
32
10
|
type DisconnectCallback = () => void;
|
|
33
11
|
type WelcomeCallback = (agentId: string) => void;
|
|
34
|
-
type FatalCallback = (reason: string) => void;
|
|
35
|
-
type ModeChangeCallback = (mode: "sse" | "polling") => void;
|
|
12
|
+
type FatalCallback = (reason: string, code?: number) => void;
|
|
36
13
|
|
|
37
14
|
export type ClawroomClientOptions = {
|
|
38
15
|
endpoint: string;
|
|
@@ -40,70 +17,47 @@ export type ClawroomClientOptions = {
|
|
|
40
17
|
deviceId: string;
|
|
41
18
|
skills: string[];
|
|
42
19
|
log?: {
|
|
43
|
-
info?: (...args: unknown[]) => void;
|
|
44
|
-
warn?: (...args: unknown[]) => void;
|
|
45
|
-
error?: (...args: unknown[]) => void;
|
|
20
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
21
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
22
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
46
23
|
};
|
|
47
24
|
};
|
|
48
25
|
|
|
49
26
|
/**
|
|
50
|
-
*
|
|
27
|
+
* ClawRoom agent client using HTTP polling.
|
|
51
28
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Agent→Server actions always use HTTP POST:
|
|
56
|
-
* /api/agents/heartbeat, /complete, /fail, /progress, /claim
|
|
29
|
+
* Agent→Server actions use HTTP POST:
|
|
30
|
+
* /api/agents/heartbeat, /poll, /complete, /fail, /progress, /claim
|
|
57
31
|
*/
|
|
58
32
|
export class ClawroomClient {
|
|
59
33
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
60
34
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
|
-
private reconnectAttempt = 0;
|
|
62
|
-
private consecutiveSseFails = 0;
|
|
63
|
-
private reconnecting = false;
|
|
64
35
|
private stopped = false;
|
|
65
|
-
private
|
|
66
|
-
private pollCycleCount = 0;
|
|
36
|
+
private connected = false;
|
|
67
37
|
private httpBase: string;
|
|
68
38
|
|
|
69
39
|
private taskCallbacks: TaskCallback[] = [];
|
|
70
|
-
private taskListCallbacks: TaskListCallback[] = [];
|
|
71
40
|
private claimAckCallbacks: ClaimAckCallback[] = [];
|
|
72
|
-
private claimRequestCallbacks: ClaimRequestCallback[] = [];
|
|
73
|
-
private errorCallbacks: ErrorCallback[] = [];
|
|
74
41
|
private disconnectCallbacks: DisconnectCallback[] = [];
|
|
75
42
|
private welcomeCallbacks: WelcomeCallback[] = [];
|
|
76
43
|
private fatalCallbacks: FatalCallback[] = [];
|
|
77
|
-
private modeChangeCallbacks: ModeChangeCallback[] = [];
|
|
78
44
|
|
|
79
45
|
constructor(private readonly options: ClawroomClientOptions) {
|
|
80
|
-
|
|
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
|
-
const ep = options.endpoint;
|
|
84
|
-
if (ep.includes("/api/agents")) {
|
|
85
|
-
this.httpBase = ep.replace(/\/stream\/?$/, "");
|
|
86
|
-
} else {
|
|
87
|
-
const httpUrl = ep.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
88
|
-
this.httpBase = httpUrl.replace(/\/ws\/.*$/, "/api/agents");
|
|
89
|
-
}
|
|
46
|
+
this.httpBase = options.endpoint.replace(/\/+$/, "");
|
|
90
47
|
}
|
|
91
48
|
|
|
92
49
|
connect(): void {
|
|
93
50
|
this.stopped = false;
|
|
94
|
-
this.reconnecting = false;
|
|
95
|
-
this.reconnectAttempt = 0;
|
|
96
|
-
this.consecutiveSseFails = 0;
|
|
97
|
-
this.mode = "sse";
|
|
98
|
-
this.doConnectSSE();
|
|
99
51
|
this.startHeartbeat();
|
|
52
|
+
this.startPolling();
|
|
53
|
+
void this.register();
|
|
100
54
|
}
|
|
101
55
|
|
|
102
56
|
disconnect(): void {
|
|
103
57
|
this.stopped = true;
|
|
104
58
|
this.stopHeartbeat();
|
|
105
59
|
this.stopPolling();
|
|
106
|
-
this.
|
|
60
|
+
this.markDisconnected();
|
|
107
61
|
}
|
|
108
62
|
|
|
109
63
|
send(message: AgentMessage): void {
|
|
@@ -113,30 +67,22 @@ export class ClawroomClient {
|
|
|
113
67
|
}
|
|
114
68
|
|
|
115
69
|
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
|
-
}
|
|
70
|
+
get isConnected(): boolean { return this.connected; }
|
|
120
71
|
get isFatal(): boolean { return false; }
|
|
121
|
-
get currentMode(): "sse" | "polling" { return this.mode; }
|
|
122
72
|
|
|
123
73
|
onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
|
|
124
|
-
onTaskList(cb: TaskListCallback): void { this.taskListCallbacks.push(cb); }
|
|
125
74
|
onClaimAck(cb: ClaimAckCallback): void { this.claimAckCallbacks.push(cb); }
|
|
126
|
-
onClaimRequest(cb: ClaimRequestCallback): void { this.claimRequestCallbacks.push(cb); }
|
|
127
|
-
onError(cb: ErrorCallback): void { this.errorCallbacks.push(cb); }
|
|
128
75
|
onDisconnect(cb: DisconnectCallback): void { this.disconnectCallbacks.push(cb); }
|
|
129
76
|
onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
|
|
130
77
|
onFatal(cb: FatalCallback): void { this.fatalCallbacks.push(cb); }
|
|
131
|
-
onModeChange(cb: ModeChangeCallback): void { this.modeChangeCallbacks.push(cb); }
|
|
132
78
|
|
|
133
|
-
// ── Heartbeat (HTTP POST
|
|
79
|
+
// ── Heartbeat (HTTP POST) ───────────────────────────────────────
|
|
134
80
|
|
|
135
81
|
private startHeartbeat(): void {
|
|
136
82
|
this.stopHeartbeat();
|
|
137
83
|
this.heartbeatTimer = setInterval(() => {
|
|
138
84
|
if (this.stopped) return;
|
|
139
|
-
this.
|
|
85
|
+
void this.register();
|
|
140
86
|
}, HEARTBEAT_INTERVAL_MS);
|
|
141
87
|
}
|
|
142
88
|
|
|
@@ -144,147 +90,60 @@ export class ClawroomClient {
|
|
|
144
90
|
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
145
91
|
}
|
|
146
92
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const { token, deviceId, skills } = this.options;
|
|
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;
|
|
93
|
+
private markConnected(agentId?: string): void {
|
|
94
|
+
if (this.connected) return;
|
|
95
|
+
this.connected = true;
|
|
96
|
+
this.options.log?.info?.("[clawroom] polling connected");
|
|
97
|
+
if (agentId) {
|
|
98
|
+
for (const cb of this.welcomeCallbacks) cb(agentId);
|
|
231
99
|
}
|
|
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
100
|
}
|
|
243
101
|
|
|
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);
|
|
102
|
+
private markDisconnected(): void {
|
|
103
|
+
if (!this.connected) return;
|
|
104
|
+
this.connected = false;
|
|
105
|
+
for (const cb of this.disconnectCallbacks) cb();
|
|
255
106
|
}
|
|
256
107
|
|
|
257
|
-
private
|
|
258
|
-
this.options.log?.info?.("[clawroom] SSE restored, switching back from polling");
|
|
108
|
+
private startPolling(): void {
|
|
259
109
|
this.stopPolling();
|
|
260
|
-
this.
|
|
261
|
-
|
|
110
|
+
this.options.log?.info?.(`[clawroom] polling ${this.httpBase}/poll`);
|
|
111
|
+
this.pollTimer = setInterval(() => {
|
|
112
|
+
void this.pollTick();
|
|
113
|
+
}, POLL_INTERVAL_MS);
|
|
262
114
|
}
|
|
263
115
|
|
|
264
116
|
private stopPolling(): void {
|
|
265
117
|
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
266
118
|
}
|
|
267
119
|
|
|
120
|
+
private async register(): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
const res = await this.httpRequest("POST", "/heartbeat", {
|
|
123
|
+
deviceId: this.options.deviceId,
|
|
124
|
+
skills: this.options.skills,
|
|
125
|
+
});
|
|
126
|
+
this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
268
132
|
private async pollTick(): Promise<void> {
|
|
269
133
|
if (this.stopped) return;
|
|
270
|
-
this.pollCycleCount++;
|
|
271
134
|
|
|
272
135
|
try {
|
|
273
136
|
const res = await this.httpRequest("POST", "/poll", {});
|
|
137
|
+
this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
|
|
274
138
|
if (res.task) {
|
|
275
139
|
const task = res.task as ServerTask;
|
|
276
|
-
this.options.log?.info?.(`[clawroom]
|
|
140
|
+
this.options.log?.info?.(`[clawroom] received task ${task.taskId}: ${task.title}`);
|
|
277
141
|
for (const cb of this.taskCallbacks) cb(task);
|
|
278
142
|
for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: task.taskId, ok: true });
|
|
279
143
|
}
|
|
280
144
|
} 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();
|
|
145
|
+
this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
|
|
146
|
+
this.markDisconnected();
|
|
288
147
|
}
|
|
289
148
|
}
|
|
290
149
|
|
|
@@ -338,45 +197,11 @@ export class ClawroomClient {
|
|
|
338
197
|
this.stopped = true;
|
|
339
198
|
this.stopHeartbeat();
|
|
340
199
|
this.stopPolling();
|
|
341
|
-
this.
|
|
342
|
-
for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}
|
|
200
|
+
this.markDisconnected();
|
|
201
|
+
for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`, 401);
|
|
343
202
|
}
|
|
344
203
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
345
204
|
}
|
|
346
205
|
return res.json();
|
|
347
206
|
}
|
|
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
207
|
}
|
package/src/task-executor.ts
CHANGED
|
@@ -12,24 +12,26 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Wire up the task execution pipeline:
|
|
15
|
-
* 1. On server.task
|
|
15
|
+
* 1. On server.task -> 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;
|
|
22
23
|
runtime: PluginRuntime;
|
|
23
24
|
log?: {
|
|
24
|
-
info?: (...args: unknown[]) => void;
|
|
25
|
-
warn?: (...args: unknown[]) => void;
|
|
26
|
-
error?: (...args: unknown[]) => void;
|
|
25
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
26
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
27
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
27
28
|
};
|
|
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) => {
|
|
@@ -37,15 +39,6 @@ export function setupTaskExecutor(opts: {
|
|
|
37
39
|
knownTasks.set(task.taskId, task);
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
// Task list on connect — store all
|
|
41
|
-
client.onTaskList((tasks: ServerTask[]) => {
|
|
42
|
-
log?.info?.(`[clawroom:executor] ${tasks.length} open task(s) available`);
|
|
43
|
-
for (const t of tasks) {
|
|
44
|
-
log?.info?.(`[clawroom:executor] - ${t.taskId}: ${t.title}`);
|
|
45
|
-
knownTasks.set(t.taskId, t);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
42
|
// Claim ack — either from plugin-initiated claim or Dashboard-initiated claim
|
|
50
43
|
client.onClaimAck((ack: ServerClaimAck) => {
|
|
51
44
|
if (!ack.ok) {
|
|
@@ -59,6 +52,11 @@ export function setupTaskExecutor(opts: {
|
|
|
59
52
|
const task = knownTasks.get(ack.taskId);
|
|
60
53
|
knownTasks.delete(ack.taskId);
|
|
61
54
|
|
|
55
|
+
if (activeTasks.has(ack.taskId)) {
|
|
56
|
+
log?.info?.(`[clawroom:executor] task ${ack.taskId} is already running, ignoring duplicate dispatch`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
62
60
|
if (!task) {
|
|
63
61
|
log?.warn?.(
|
|
64
62
|
`[clawroom:executor] claim_ack for unknown task ${ack.taskId}`,
|
|
@@ -67,7 +65,10 @@ export function setupTaskExecutor(opts: {
|
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
log?.info?.(`[clawroom:executor] executing task ${task.taskId}`);
|
|
70
|
-
|
|
68
|
+
activeTasks.add(task.taskId);
|
|
69
|
+
void executeTask({ client, runtime, task, log }).finally(() => {
|
|
70
|
+
activeTasks.delete(task.taskId);
|
|
71
|
+
});
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -80,8 +81,8 @@ async function executeTask(opts: {
|
|
|
80
81
|
runtime: PluginRuntime;
|
|
81
82
|
task: ServerTask;
|
|
82
83
|
log?: {
|
|
83
|
-
info?: (...args: unknown[]) => void;
|
|
84
|
-
error?: (...args: unknown[]) => void;
|
|
84
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
85
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
85
86
|
};
|
|
86
87
|
}): Promise<void> {
|
|
87
88
|
const { client, runtime, task, log } = opts;
|
|
@@ -97,7 +98,7 @@ async function executeTask(opts: {
|
|
|
97
98
|
idempotencyKey: `clawroom:${task.taskId}`,
|
|
98
99
|
message: agentMessage,
|
|
99
100
|
extraSystemPrompt:
|
|
100
|
-
"You are executing a task from the
|
|
101
|
+
"You are executing a task from the ClawRoom marketplace. " +
|
|
101
102
|
"Complete the task and provide a SHORT summary (2-3 sentences) of what you did. " +
|
|
102
103
|
"Do NOT include any local file paths, machine info, or internal details in your summary. " +
|
|
103
104
|
"If you create output files, list their absolute paths at the very end, " +
|
|
@@ -170,7 +171,7 @@ async function executeTask(opts: {
|
|
|
170
171
|
const reason = err instanceof Error ? err.message : String(err);
|
|
171
172
|
log?.error?.(`[clawroom:executor] unexpected error for task ${task.taskId}: ${reason}`);
|
|
172
173
|
client.send({
|
|
173
|
-
type: "agent.
|
|
174
|
+
type: "agent.fail",
|
|
174
175
|
taskId: task.taskId,
|
|
175
176
|
reason,
|
|
176
177
|
});
|
|
@@ -279,7 +280,10 @@ function tryParse(s: string): unknown {
|
|
|
279
280
|
*/
|
|
280
281
|
function readAllFiles(
|
|
281
282
|
paths: string[],
|
|
282
|
-
log?: {
|
|
283
|
+
log?: {
|
|
284
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
285
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
286
|
+
},
|
|
283
287
|
): AgentResultFile[] {
|
|
284
288
|
const results: AgentResultFile[] = [];
|
|
285
289
|
for (const fp of paths) {
|