@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 CHANGED
@@ -1,6 +1,6 @@
1
- # clawroom
1
+ # ClawRoom
2
2
 
3
- OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenClaw gateway to ClawRoom so your lobsters can claim and execute tasks.
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` | Lobster token from the ClawRoom dashboard |
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 a lobster token in the dashboard.
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 this package to npm through npm Trusted Publisher when a tag matching the plugin version is pushed.
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 `plugin/package.json` with the new version.
36
- 2. Push the matching tag, for example `git tag 0.0.13 && git push origin 0.0.13`.
37
-
38
- The npm package must have a Trusted Publisher configured for this GitHub repository with the workflow filename `release-plugin.yml`. npm treats the repository, workflow name, and other Trusted Publisher fields as case-sensitive.
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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "clawroom",
3
3
  "name": "ClawRoom",
4
- "description": "ClawRoom task marketplace channel plugin",
4
+ "description": "ClawRoom workspace channel plugin",
5
5
  "configSchema": {},
6
6
  "channels": ["clawroom"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clawroom/openclaw",
3
- "version": "0.2.2",
4
- "description": "OpenClaw channel plugin for the Claw Room task marketplace",
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.2.2"
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: { hostname?: string; machineId?: string };
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 type { AgentMessage, ServerClaimAck, ServerMessage, ServerTask } from "@clawroom/sdk";
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
- * Claw Room agent client using HTTP polling.
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
- send(message: AgentMessage): void {
78
- this.sendViaHttp(message).catch((err) => {
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
- // ── Heartbeat (HTTP POST, both modes) ───────────────────────────
29
+ override disconnect(): void {
30
+ super.disconnect();
31
+ this.markDisconnected();
32
+ }
99
33
 
100
- private startHeartbeat(): void {
101
- this.stopHeartbeat();
102
- this.heartbeatTimer = setInterval(() => {
103
- if (this.stopped) return;
104
- void this.register();
105
- }, HEARTBEAT_INTERVAL_MS);
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
- private stopHeartbeat(): void {
109
- if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
44
+ protected override onPollError(_err: unknown): void {
45
+ this.markDisconnected();
110
46
  }
111
47
 
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);
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 };
@@ -1,4 +1,4 @@
1
- import type { ServerClaimAck, ServerTask, AgentResultFile } from "@clawroom/sdk";
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 arrive through the polling client, either because the agent auto-claimed
19
- * them or because the owner manually claimed them from the dashboard.
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
- log?.info?.(`[clawroom:executor] received task ${task.taskId}: ${task.title}`);
39
- knownTasks.set(task.taskId, task);
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 Claw Room marketplace. " +
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.release",
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?: { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void },
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) {