@clawroom/openclaw 0.2.2 → 0.3.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 +9 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -4
- package/src/channel.ts +3 -1
- package/src/chat-executor.ts +159 -0
- package/src/client.ts +29 -193
- package/src/task-executor.ts +18 -54
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClawRoom
|
|
2
2
|
|
|
3
|
-
OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenClaw gateway to ClawRoom so your
|
|
3
|
+
OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenClaw gateway to ClawRoom so your agents can claim and execute tasks.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,25 +14,25 @@ openclaw gateway restart
|
|
|
14
14
|
|
|
15
15
|
| Key | Description |
|
|
16
16
|
|---|---|
|
|
17
|
-
| `channels.clawroom.token` |
|
|
17
|
+
| `channels.clawroom.token` | Agent token from the ClawRoom dashboard |
|
|
18
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
|
|
|
22
22
|
## How it works
|
|
23
23
|
|
|
24
|
-
1. Sign up at ClawRoom and create
|
|
24
|
+
1. Sign up at ClawRoom and create an agent token in the dashboard.
|
|
25
25
|
2. Install this plugin and configure the token.
|
|
26
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/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawroom/openclaw",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenClaw channel plugin for
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "OpenClaw channel plugin for ClawRoom",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -12,15 +12,17 @@
|
|
|
12
12
|
"openclaw",
|
|
13
13
|
"clawroom",
|
|
14
14
|
"plugin",
|
|
15
|
-
"marketplace",
|
|
16
15
|
"agent"
|
|
17
16
|
],
|
|
18
17
|
"files": [
|
|
19
18
|
"src",
|
|
20
19
|
"openclaw.plugin.json"
|
|
21
20
|
],
|
|
21
|
+
"bundleDependencies": [
|
|
22
|
+
"@clawroom/sdk"
|
|
23
|
+
],
|
|
22
24
|
"dependencies": {
|
|
23
|
-
"@clawroom/sdk": "^0.
|
|
25
|
+
"@clawroom/sdk": "^0.3.0"
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
26
28
|
"openclaw": "*"
|
package/src/channel.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { collectSkills } from "./skill-reporter.js";
|
|
|
4
4
|
import { getClawroomRuntime } from "./runtime.js";
|
|
5
5
|
import { ClawroomClient } from "./client.js";
|
|
6
6
|
import { setupTaskExecutor } from "./task-executor.js";
|
|
7
|
+
import { setupChatExecutor } from "./chat-executor.js";
|
|
7
8
|
|
|
8
9
|
// ── Config resolution ────────────────────────────────────────────────
|
|
9
10
|
|
|
@@ -197,6 +198,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
197
198
|
});
|
|
198
199
|
|
|
199
200
|
setupTaskExecutor({ client: client, runtime, log });
|
|
201
|
+
setupChatExecutor({ client: client, runtime, log });
|
|
200
202
|
client.connect();
|
|
201
203
|
activeClient = client;
|
|
202
204
|
|
|
@@ -268,7 +270,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
268
270
|
* runtime environment when available, falling back to a random id.
|
|
269
271
|
*/
|
|
270
272
|
function resolveDeviceId(ctx: {
|
|
271
|
-
runtime
|
|
273
|
+
runtime?: unknown;
|
|
272
274
|
}): string {
|
|
273
275
|
// The RuntimeEnv may expose hostname or machineId depending on version
|
|
274
276
|
const r = ctx.runtime as Record<string, unknown>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ServerChatMessage } from "@clawroom/sdk";
|
|
2
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import type { ClawroomClient } from "./client.js";
|
|
4
|
+
|
|
5
|
+
/** Default timeout for chat reply subagent (2 minutes). */
|
|
6
|
+
const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wire up the chat execution pipeline:
|
|
10
|
+
* 1. On chat mention → send typing indicator
|
|
11
|
+
* 2. Run subagent with channel context
|
|
12
|
+
* 3. Send reply back to channel
|
|
13
|
+
*/
|
|
14
|
+
export function setupChatExecutor(opts: {
|
|
15
|
+
client: ClawroomClient;
|
|
16
|
+
runtime: PluginRuntime;
|
|
17
|
+
log?: {
|
|
18
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
19
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
20
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
21
|
+
};
|
|
22
|
+
}): void {
|
|
23
|
+
const { client, runtime, log } = opts;
|
|
24
|
+
|
|
25
|
+
client.onChatMessage((messages: ServerChatMessage[]) => {
|
|
26
|
+
for (const msg of messages) {
|
|
27
|
+
void handleChatMention({ client, runtime, msg, log });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function handleChatMention(opts: {
|
|
33
|
+
client: ClawroomClient;
|
|
34
|
+
runtime: PluginRuntime;
|
|
35
|
+
msg: ServerChatMessage;
|
|
36
|
+
log?: {
|
|
37
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
38
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
39
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
40
|
+
};
|
|
41
|
+
}): Promise<void> {
|
|
42
|
+
const { client, runtime, msg, log } = opts;
|
|
43
|
+
const sessionKey = `clawroom:chat:${msg.channelId}:${msg.messageId}`;
|
|
44
|
+
|
|
45
|
+
log?.info?.(`[clawroom:chat] processing mention in channel ${msg.channelId}: "${msg.content.slice(0, 80)}"`);
|
|
46
|
+
|
|
47
|
+
// Send typing indicator
|
|
48
|
+
client.send({ type: "agent.typing", channelId: msg.channelId });
|
|
49
|
+
|
|
50
|
+
// Build context from recent messages
|
|
51
|
+
const contextLines = msg.context
|
|
52
|
+
.map((c) => `[${c.senderName}]: ${c.content}`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
|
|
55
|
+
const agentMessage = [
|
|
56
|
+
"You were mentioned in a chat channel. Reply concisely and helpfully.",
|
|
57
|
+
"",
|
|
58
|
+
"## Recent messages",
|
|
59
|
+
contextLines,
|
|
60
|
+
"",
|
|
61
|
+
"## Message that mentioned you",
|
|
62
|
+
msg.content,
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Keep typing indicator alive during execution
|
|
67
|
+
const typingInterval = setInterval(() => {
|
|
68
|
+
client.send({ type: "agent.typing", channelId: msg.channelId });
|
|
69
|
+
}, 3000);
|
|
70
|
+
|
|
71
|
+
const { runId } = await runtime.subagent.run({
|
|
72
|
+
sessionKey,
|
|
73
|
+
idempotencyKey: `clawroom:chat:${msg.messageId}`,
|
|
74
|
+
message: agentMessage,
|
|
75
|
+
extraSystemPrompt:
|
|
76
|
+
"You are responding to a chat @mention in ClawRoom. " +
|
|
77
|
+
"Keep your reply SHORT and conversational (1-3 sentences). " +
|
|
78
|
+
"Do NOT include file paths, system info, or markdown headers. " +
|
|
79
|
+
"Just answer naturally like a helpful teammate.",
|
|
80
|
+
lane: "clawroom",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const waitResult = await runtime.subagent.waitForRun({
|
|
84
|
+
runId,
|
|
85
|
+
timeoutMs: CHAT_TIMEOUT_MS,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
clearInterval(typingInterval);
|
|
89
|
+
|
|
90
|
+
if (waitResult.status === "error") {
|
|
91
|
+
log?.error?.(`[clawroom:chat] subagent error: ${waitResult.error}`);
|
|
92
|
+
client.send({
|
|
93
|
+
type: "agent.chat.reply",
|
|
94
|
+
channelId: msg.channelId,
|
|
95
|
+
content: "Sorry, I encountered an error processing your message.",
|
|
96
|
+
replyTo: msg.messageId,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (waitResult.status === "timeout") {
|
|
102
|
+
log?.error?.(`[clawroom:chat] subagent timeout for message ${msg.messageId}`);
|
|
103
|
+
client.send({
|
|
104
|
+
type: "agent.chat.reply",
|
|
105
|
+
channelId: msg.channelId,
|
|
106
|
+
content: "Sorry, I took too long to respond. Please try again.",
|
|
107
|
+
replyTo: msg.messageId,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { messages } = await runtime.subagent.getSessionMessages({
|
|
113
|
+
sessionKey,
|
|
114
|
+
limit: 10,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const reply = extractLastAssistantMessage(messages);
|
|
118
|
+
|
|
119
|
+
log?.info?.(`[clawroom:chat] replying to ${msg.messageId}: "${reply.slice(0, 80)}"`);
|
|
120
|
+
client.send({
|
|
121
|
+
type: "agent.chat.reply",
|
|
122
|
+
channelId: msg.channelId,
|
|
123
|
+
content: reply,
|
|
124
|
+
replyTo: msg.messageId,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await runtime.subagent.deleteSession({
|
|
128
|
+
sessionKey,
|
|
129
|
+
deleteTranscript: true,
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
133
|
+
log?.error?.(`[clawroom:chat] unexpected error: ${reason}`);
|
|
134
|
+
client.send({
|
|
135
|
+
type: "agent.chat.reply",
|
|
136
|
+
channelId: msg.channelId,
|
|
137
|
+
content: "Sorry, something went wrong. Please try again.",
|
|
138
|
+
replyTo: msg.messageId,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractLastAssistantMessage(messages: unknown[]): string {
|
|
144
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
145
|
+
const msg = messages[i] as Record<string, unknown> | undefined;
|
|
146
|
+
if (!msg || msg.role !== "assistant") continue;
|
|
147
|
+
|
|
148
|
+
if (typeof msg.content === "string") return msg.content;
|
|
149
|
+
if (Array.isArray(msg.content)) {
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
for (const block of msg.content) {
|
|
152
|
+
const b = block as Record<string, unknown>;
|
|
153
|
+
if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
154
|
+
}
|
|
155
|
+
if (parts.length > 0) return parts.join("\n");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return "I'm not sure how to respond to that.";
|
|
159
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,121 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ClawroomClient as BaseClient } from "@clawroom/sdk";
|
|
2
|
+
import type { ClawroomClientOptions } from "@clawroom/sdk";
|
|
2
3
|
|
|
3
|
-
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
4
|
-
const POLL_INTERVAL_MS = 10_000;
|
|
5
|
-
|
|
6
|
-
// ── Types ─────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
type TaskCallback = (task: ServerTask) => void;
|
|
9
|
-
type TaskListCallback = (tasks: ServerTask[]) => void;
|
|
10
|
-
type ClaimAckCallback = (ack: ServerClaimAck) => void;
|
|
11
|
-
type ClaimRequestCallback = (task: ServerTask) => void;
|
|
12
|
-
type ErrorCallback = (error: ServerMessage & { type: "server.error" }) => void;
|
|
13
4
|
type DisconnectCallback = () => void;
|
|
14
5
|
type WelcomeCallback = (agentId: string) => void;
|
|
15
|
-
type FatalCallback = (reason: string) => void;
|
|
16
|
-
type ModeChangeCallback = (mode: "sse" | "polling") => void;
|
|
17
|
-
|
|
18
|
-
export type ClawroomClientOptions = {
|
|
19
|
-
endpoint: string;
|
|
20
|
-
token: string;
|
|
21
|
-
deviceId: string;
|
|
22
|
-
skills: string[];
|
|
23
|
-
log?: {
|
|
24
|
-
info?: (...args: unknown[]) => void;
|
|
25
|
-
warn?: (...args: unknown[]) => void;
|
|
26
|
-
error?: (...args: unknown[]) => void;
|
|
27
|
-
};
|
|
28
|
-
};
|
|
6
|
+
type FatalCallback = (reason: string, code?: number) => void;
|
|
29
7
|
|
|
30
8
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Agent→Server actions use HTTP POST:
|
|
34
|
-
* /api/agents/heartbeat, /poll, /complete, /fail, /progress, /claim
|
|
9
|
+
* Plugin-specific ClawRoom client that extends the SDK client
|
|
10
|
+
* with OpenClaw lifecycle hooks (connected state, disconnect, fatal).
|
|
35
11
|
*/
|
|
36
|
-
export class ClawroomClient {
|
|
37
|
-
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
38
|
-
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
39
|
-
private stopped = false;
|
|
12
|
+
export class ClawroomClient extends BaseClient {
|
|
40
13
|
private connected = false;
|
|
41
|
-
private httpBase: string;
|
|
42
|
-
|
|
43
|
-
private taskCallbacks: TaskCallback[] = [];
|
|
44
|
-
private taskListCallbacks: TaskListCallback[] = [];
|
|
45
|
-
private claimAckCallbacks: ClaimAckCallback[] = [];
|
|
46
|
-
private claimRequestCallbacks: ClaimRequestCallback[] = [];
|
|
47
|
-
private errorCallbacks: ErrorCallback[] = [];
|
|
48
14
|
private disconnectCallbacks: DisconnectCallback[] = [];
|
|
49
15
|
private welcomeCallbacks: WelcomeCallback[] = [];
|
|
50
16
|
private fatalCallbacks: FatalCallback[] = [];
|
|
51
|
-
private modeChangeCallbacks: ModeChangeCallback[] = [];
|
|
52
|
-
|
|
53
|
-
constructor(private readonly options: ClawroomClientOptions) {
|
|
54
|
-
const ep = options.endpoint;
|
|
55
|
-
if (ep.includes("/api/agents")) {
|
|
56
|
-
this.httpBase = ep.replace(/\/stream\/?$/, "");
|
|
57
|
-
} else {
|
|
58
|
-
const httpUrl = ep.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
59
|
-
this.httpBase = httpUrl.replace(/\/ws\/.*$/, "/api/agents");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
connect(): void {
|
|
64
|
-
this.stopped = false;
|
|
65
|
-
this.startHeartbeat();
|
|
66
|
-
this.startPolling();
|
|
67
|
-
void this.register();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
disconnect(): void {
|
|
71
|
-
this.stopped = true;
|
|
72
|
-
this.stopHeartbeat();
|
|
73
|
-
this.stopPolling();
|
|
74
|
-
this.markDisconnected();
|
|
75
|
-
}
|
|
76
17
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.options.log?.warn?.(`[clawroom] send error: ${err}`);
|
|
80
|
-
});
|
|
18
|
+
constructor(options: ClawroomClientOptions) {
|
|
19
|
+
super(options);
|
|
81
20
|
}
|
|
82
21
|
|
|
83
22
|
get isAlive(): boolean { return !this.stopped; }
|
|
84
23
|
get isConnected(): boolean { return this.connected; }
|
|
85
|
-
get isFatal(): boolean { return false; }
|
|
86
|
-
get currentMode(): "sse" | "polling" { return "polling"; }
|
|
87
24
|
|
|
88
|
-
onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
|
|
89
|
-
onTaskList(cb: TaskListCallback): void { this.taskListCallbacks.push(cb); }
|
|
90
|
-
onClaimAck(cb: ClaimAckCallback): void { this.claimAckCallbacks.push(cb); }
|
|
91
|
-
onClaimRequest(cb: ClaimRequestCallback): void { this.claimRequestCallbacks.push(cb); }
|
|
92
|
-
onError(cb: ErrorCallback): void { this.errorCallbacks.push(cb); }
|
|
93
25
|
onDisconnect(cb: DisconnectCallback): void { this.disconnectCallbacks.push(cb); }
|
|
94
26
|
onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
|
|
95
27
|
onFatal(cb: FatalCallback): void { this.fatalCallbacks.push(cb); }
|
|
96
|
-
onModeChange(cb: ModeChangeCallback): void { this.modeChangeCallbacks.push(cb); }
|
|
97
28
|
|
|
98
|
-
|
|
29
|
+
override disconnect(): void {
|
|
30
|
+
super.disconnect();
|
|
31
|
+
this.markDisconnected();
|
|
32
|
+
}
|
|
99
33
|
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
34
|
+
protected override onPollSuccess(agentId: string | undefined): void {
|
|
35
|
+
if (!this.connected) {
|
|
36
|
+
this.connected = true;
|
|
37
|
+
this.options.log?.info?.("[clawroom] polling connected");
|
|
38
|
+
if (agentId) {
|
|
39
|
+
for (const cb of this.welcomeCallbacks) cb(agentId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
106
42
|
}
|
|
107
43
|
|
|
108
|
-
|
|
109
|
-
|
|
44
|
+
protected override onPollError(_err: unknown): void {
|
|
45
|
+
this.markDisconnected();
|
|
110
46
|
}
|
|
111
47
|
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (agentId) {
|
|
118
|
-
for (const cb of this.welcomeCallbacks) cb(agentId);
|
|
48
|
+
protected override onHttpError(status: number, text: string): void {
|
|
49
|
+
if (status === 401) {
|
|
50
|
+
this.stopped = true;
|
|
51
|
+
this.markDisconnected();
|
|
52
|
+
for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`, 401);
|
|
119
53
|
}
|
|
120
54
|
}
|
|
121
55
|
|
|
@@ -124,104 +58,6 @@ export class ClawroomClient {
|
|
|
124
58
|
this.connected = false;
|
|
125
59
|
for (const cb of this.disconnectCallbacks) cb();
|
|
126
60
|
}
|
|
127
|
-
|
|
128
|
-
private startPolling(): void {
|
|
129
|
-
this.stopPolling();
|
|
130
|
-
this.options.log?.info?.(`[clawroom] polling ${this.httpBase}/poll`);
|
|
131
|
-
this.pollTimer = setInterval(() => {
|
|
132
|
-
void this.pollTick();
|
|
133
|
-
}, POLL_INTERVAL_MS);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private stopPolling(): void {
|
|
137
|
-
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
138
|
-
}
|
|
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
|
-
|
|
152
|
-
private async pollTick(): Promise<void> {
|
|
153
|
-
if (this.stopped) return;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const res = await this.httpRequest("POST", "/poll", {});
|
|
157
|
-
this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
|
|
158
|
-
if (res.task) {
|
|
159
|
-
const task = res.task as ServerTask;
|
|
160
|
-
this.options.log?.info?.(`[clawroom] received task ${task.taskId}: ${task.title}`);
|
|
161
|
-
for (const cb of this.taskCallbacks) cb(task);
|
|
162
|
-
for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: task.taskId, ok: true });
|
|
163
|
-
}
|
|
164
|
-
} catch (err) {
|
|
165
|
-
this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
|
|
166
|
-
this.markDisconnected();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ── HTTP send ───────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
private async sendViaHttp(message: AgentMessage): Promise<void> {
|
|
173
|
-
switch (message.type) {
|
|
174
|
-
case "agent.complete":
|
|
175
|
-
await this.httpRequest("POST", "/complete", {
|
|
176
|
-
taskId: message.taskId,
|
|
177
|
-
output: message.output,
|
|
178
|
-
attachments: message.attachments,
|
|
179
|
-
});
|
|
180
|
-
break;
|
|
181
|
-
case "agent.fail":
|
|
182
|
-
await this.httpRequest("POST", "/fail", {
|
|
183
|
-
taskId: message.taskId,
|
|
184
|
-
reason: message.reason,
|
|
185
|
-
});
|
|
186
|
-
break;
|
|
187
|
-
case "agent.progress":
|
|
188
|
-
await this.httpRequest("POST", "/progress", {
|
|
189
|
-
taskId: message.taskId,
|
|
190
|
-
message: message.message,
|
|
191
|
-
percent: message.percent,
|
|
192
|
-
});
|
|
193
|
-
break;
|
|
194
|
-
case "agent.heartbeat":
|
|
195
|
-
await this.httpRequest("POST", "/heartbeat", {});
|
|
196
|
-
break;
|
|
197
|
-
case "agent.claim":
|
|
198
|
-
await this.httpRequest("POST", "/claim", { taskId: message.taskId });
|
|
199
|
-
break;
|
|
200
|
-
default:
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private async httpRequest(method: string, path: string, body: unknown): Promise<any> {
|
|
206
|
-
const res = await fetch(`${this.httpBase}${path}`, {
|
|
207
|
-
method,
|
|
208
|
-
headers: {
|
|
209
|
-
"Content-Type": "application/json",
|
|
210
|
-
"Authorization": `Bearer ${this.options.token}`,
|
|
211
|
-
},
|
|
212
|
-
body: JSON.stringify(body),
|
|
213
|
-
});
|
|
214
|
-
if (!res.ok) {
|
|
215
|
-
const text = await res.text().catch(() => "");
|
|
216
|
-
if (res.status === 401) {
|
|
217
|
-
this.stopped = true;
|
|
218
|
-
this.stopHeartbeat();
|
|
219
|
-
this.stopPolling();
|
|
220
|
-
this.markDisconnected();
|
|
221
|
-
for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`);
|
|
222
|
-
}
|
|
223
|
-
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
224
|
-
}
|
|
225
|
-
return res.json();
|
|
226
|
-
}
|
|
227
61
|
}
|
|
62
|
+
|
|
63
|
+
export type { ClawroomClientOptions };
|
package/src/task-executor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServerTask, AgentResultFile } from "@clawroom/sdk";
|
|
2
2
|
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import type { ClawroomClient } from "./client.js";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -11,69 +11,30 @@ const SUBAGENT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
11
11
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Wire up the task execution pipeline
|
|
15
|
-
* 1. On server.task / server.task_list -> store as available
|
|
16
|
-
* 2. On claim_ack(ok) -> invoke subagent -> send result/fail
|
|
14
|
+
* Wire up the task execution pipeline.
|
|
17
15
|
*
|
|
18
|
-
* Tasks
|
|
19
|
-
*
|
|
16
|
+
* Tasks are assigned by humans and delivered via /poll.
|
|
17
|
+
* On receiving an assigned task, execute it immediately via subagent.
|
|
20
18
|
*/
|
|
21
19
|
export function setupTaskExecutor(opts: {
|
|
22
20
|
client: ClawroomClient;
|
|
23
21
|
runtime: PluginRuntime;
|
|
24
22
|
log?: {
|
|
25
|
-
info?: (...args: unknown[]) => void;
|
|
26
|
-
warn?: (...args: unknown[]) => void;
|
|
27
|
-
error?: (...args: unknown[]) => void;
|
|
23
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
24
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
25
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
28
26
|
};
|
|
29
27
|
}): void {
|
|
30
28
|
const { client, runtime, log } = opts;
|
|
31
|
-
|
|
32
|
-
// Track received tasks until they transition into an active execution.
|
|
33
|
-
const knownTasks = new Map<string, ServerTask>();
|
|
34
29
|
const activeTasks = new Set<string>();
|
|
35
30
|
|
|
36
|
-
// New task received — store it for potential execution
|
|
37
31
|
client.onTask((task: ServerTask) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Task list on connect — store all
|
|
43
|
-
client.onTaskList((tasks: ServerTask[]) => {
|
|
44
|
-
log?.info?.(`[clawroom:executor] ${tasks.length} open task(s) available`);
|
|
45
|
-
for (const t of tasks) {
|
|
46
|
-
log?.info?.(`[clawroom:executor] - ${t.taskId}: ${t.title}`);
|
|
47
|
-
knownTasks.set(t.taskId, t);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Claim ack — either from plugin-initiated claim or Dashboard-initiated claim
|
|
52
|
-
client.onClaimAck((ack: ServerClaimAck) => {
|
|
53
|
-
if (!ack.ok) {
|
|
54
|
-
log?.warn?.(
|
|
55
|
-
`[clawroom:executor] claim rejected for ${ack.taskId}: ${ack.reason ?? "unknown"}`,
|
|
56
|
-
);
|
|
57
|
-
knownTasks.delete(ack.taskId);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const task = knownTasks.get(ack.taskId);
|
|
62
|
-
knownTasks.delete(ack.taskId);
|
|
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
|
-
|
|
69
|
-
if (!task) {
|
|
70
|
-
log?.warn?.(
|
|
71
|
-
`[clawroom:executor] claim_ack for unknown task ${ack.taskId}`,
|
|
72
|
-
);
|
|
32
|
+
if (activeTasks.has(task.taskId)) {
|
|
33
|
+
log?.info?.(`[clawroom:executor] task ${task.taskId} already running, skipping`);
|
|
73
34
|
return;
|
|
74
35
|
}
|
|
75
36
|
|
|
76
|
-
log?.info?.(`[clawroom:executor] executing task ${task.taskId}`);
|
|
37
|
+
log?.info?.(`[clawroom:executor] executing assigned task ${task.taskId}: ${task.title}`);
|
|
77
38
|
activeTasks.add(task.taskId);
|
|
78
39
|
void executeTask({ client, runtime, task, log }).finally(() => {
|
|
79
40
|
activeTasks.delete(task.taskId);
|
|
@@ -90,8 +51,8 @@ async function executeTask(opts: {
|
|
|
90
51
|
runtime: PluginRuntime;
|
|
91
52
|
task: ServerTask;
|
|
92
53
|
log?: {
|
|
93
|
-
info?: (...args: unknown[]) => void;
|
|
94
|
-
error?: (...args: unknown[]) => void;
|
|
54
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
55
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
95
56
|
};
|
|
96
57
|
}): Promise<void> {
|
|
97
58
|
const { client, runtime, task, log } = opts;
|
|
@@ -107,7 +68,7 @@ async function executeTask(opts: {
|
|
|
107
68
|
idempotencyKey: `clawroom:${task.taskId}`,
|
|
108
69
|
message: agentMessage,
|
|
109
70
|
extraSystemPrompt:
|
|
110
|
-
"You are executing a task from the
|
|
71
|
+
"You are executing a task from the ClawRoom marketplace. " +
|
|
111
72
|
"Complete the task and provide a SHORT summary (2-3 sentences) of what you did. " +
|
|
112
73
|
"Do NOT include any local file paths, machine info, or internal details in your summary. " +
|
|
113
74
|
"If you create output files, list their absolute paths at the very end, " +
|
|
@@ -180,7 +141,7 @@ async function executeTask(opts: {
|
|
|
180
141
|
const reason = err instanceof Error ? err.message : String(err);
|
|
181
142
|
log?.error?.(`[clawroom:executor] unexpected error for task ${task.taskId}: ${reason}`);
|
|
182
143
|
client.send({
|
|
183
|
-
type: "agent.
|
|
144
|
+
type: "agent.fail",
|
|
184
145
|
taskId: task.taskId,
|
|
185
146
|
reason,
|
|
186
147
|
});
|
|
@@ -289,7 +250,10 @@ function tryParse(s: string): unknown {
|
|
|
289
250
|
*/
|
|
290
251
|
function readAllFiles(
|
|
291
252
|
paths: string[],
|
|
292
|
-
log?: {
|
|
253
|
+
log?: {
|
|
254
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
255
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
256
|
+
},
|
|
293
257
|
): AgentResultFile[] {
|
|
294
258
|
const results: AgentResultFile[] = [];
|
|
295
259
|
for (const fp of paths) {
|