@clawroom/openclaw 0.3.0 → 0.5.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
@@ -7,6 +7,7 @@ OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenCla
7
7
  ```sh
8
8
  openclaw plugins install @clawroom/openclaw
9
9
  openclaw config set channels.clawroom.token "YOUR_TOKEN"
10
+ openclaw config set channels.clawroom.endpoint "http://localhost:3000/api/agents"
10
11
  openclaw gateway restart
11
12
  ```
12
13
 
@@ -28,11 +29,11 @@ openclaw gateway restart
28
29
 
29
30
  ## Release
30
31
 
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
+ The repository includes a GitHub Actions workflow that publishes `@clawroom/protocol`, `@clawroom/sdk`, `@clawroom/bridge`, and `@clawroom/openclaw` to npm when a release tag is pushed.
32
33
 
33
34
  To publish a new version:
34
35
 
35
- 1. Update the package versions in `protocol/package.json`, `sdk/package.json`, and `plugin/package.json`.
36
+ 1. Update the package versions in `protocol/package.json`, `sdk/package.json`, `bridge/package.json`, and `plugin/package.json`.
36
37
  2. Commit and push the release commit to GitHub.
37
38
  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.
39
+ 4. Watch `.github/workflows/release-plugin.yml` until all publish jobs succeed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawroom/openclaw",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenClaw channel plugin for ClawRoom",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  "@clawroom/sdk"
23
23
  ],
24
24
  "dependencies": {
25
- "@clawroom/sdk": "^0.3.0"
25
+ "@clawroom/sdk": "^0.5.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "openclaw": "*"
package/src/channel.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
- import { collectSkills } from "./skill-reporter.js";
3
+ // import { collectSkills } from "./skill-reporter.js";
4
4
  import { getClawroomRuntime } from "./runtime.js";
5
- import { ClawroomClient } from "./client.js";
5
+ import { ClawroomPluginClient } from "./client.js";
6
6
  import { setupTaskExecutor } from "./task-executor.js";
7
7
  import { setupChatExecutor } from "./chat-executor.js";
8
8
 
9
9
  // ── Config resolution ────────────────────────────────────────────────
10
10
 
11
- const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
11
+ const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/machines";
12
12
  const DEFAULT_ACCOUNT_ID = "default";
13
13
 
14
14
  interface ClawroomAccountConfig {
15
- token?: string;
15
+ api_key?: string;
16
+ token?: string; // deprecated, fallback
16
17
  endpoint?: string;
17
18
  skills?: string[];
18
19
  enabled?: boolean;
@@ -23,7 +24,7 @@ interface ResolvedClawroomAccount {
23
24
  name: string;
24
25
  enabled: boolean;
25
26
  configured: boolean;
26
- token: string;
27
+ apiKey: string;
27
28
  endpoint: string;
28
29
  skills: string[];
29
30
  }
@@ -39,7 +40,7 @@ function resolveClawroomAccount(opts: {
39
40
  accountId?: string | null;
40
41
  }): ResolvedClawroomAccount {
41
42
  const section = readClawroomSection(opts.cfg);
42
- const token = section.token ?? "";
43
+ const apiKey = section.api_key ?? section.token ?? "";
43
44
  const endpoint = section.endpoint || DEFAULT_ENDPOINT;
44
45
  const skills = Array.isArray(section.skills) ? section.skills : [];
45
46
  const enabled = section.enabled !== false;
@@ -48,8 +49,8 @@ function resolveClawroomAccount(opts: {
48
49
  accountId: opts.accountId ?? DEFAULT_ACCOUNT_ID,
49
50
  name: "ClawRoom",
50
51
  enabled,
51
- configured: token.length > 0,
52
- token,
52
+ configured: apiKey.length > 0,
53
+ apiKey,
53
54
  endpoint,
54
55
  skills,
55
56
  };
@@ -57,7 +58,7 @@ function resolveClawroomAccount(opts: {
57
58
 
58
59
  // ── Persistent client per gateway lifecycle ───────────────────────
59
60
 
60
- let activeClient: ClawroomClient | null = null;
61
+ let activeClient: ClawroomPluginClient | null = null;
61
62
 
62
63
  // ── Channel plugin definition ────────────────────────────────────────
63
64
 
@@ -104,17 +105,9 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
104
105
  deliveryMode: "direct",
105
106
 
106
107
  sendText: async ({ to, text }) => {
107
- // Outbound messages are task results sent via HTTP.
108
- // The task-executor sends results directly through client.send();
109
- // this adapter exists for completeness if openclaw routing tries
110
- // to deliver a reply through the channel outbound path.
111
- if (activeClient) {
112
- activeClient.send({
113
- type: "agent.complete",
114
- taskId: to,
115
- output: text,
116
- });
117
- }
108
+ // Outbound path task results are sent directly by executors,
109
+ // this is a fallback if openclaw routing triggers outbound delivery.
110
+ // No-op for machine client (executors handle it).
118
111
  return { channel: "clawroom", messageId: to, to };
119
112
  },
120
113
  },
@@ -125,24 +118,16 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
125
118
 
126
119
  if (!account.configured) {
127
120
  throw new Error(
128
- "ClawRoom is not configured: set channels.clawroom.token in your OpenClaw config.",
121
+ "ClawRoom is not configured: set channels.clawroom.api_key in your OpenClaw config.",
129
122
  );
130
123
  }
131
124
 
132
125
  const runtime = getClawroomRuntime();
133
- const skills = collectSkills({
134
- runtime,
135
- configuredSkills: account.skills,
136
- });
137
-
138
- const deviceId = resolveDeviceId(ctx);
139
126
  const log = ctx.log ?? undefined;
140
127
 
141
- const client = new ClawroomClient({
128
+ const client = new ClawroomPluginClient({
142
129
  endpoint: account.endpoint,
143
- token: account.token,
144
- deviceId,
145
- skills,
130
+ apiKey: account.apiKey,
146
131
  log,
147
132
  });
148
133
 
@@ -170,23 +155,11 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
170
155
  });
171
156
  };
172
157
 
173
- // Wire up HTTP polling events to OpenClaw health status
158
+ // Wire up polling events to OpenClaw health status
174
159
  client.onWelcome(() => publishConnected());
175
- client.onTask(() => {
176
- // Any server task = update lastEventAt so gateway knows we're alive
177
- ctx.setStatus({
178
- accountId: account.accountId,
179
- running: true,
180
- connected: true,
181
- lastEventAt: Date.now(),
182
- lastStartAt: Date.now(),
183
- lastStopAt: null,
184
- lastError: null,
185
- });
186
- });
187
160
  client.onDisconnect(() => publishDisconnected());
188
- client.onFatal((reason, code) => {
189
- log?.error?.(`[clawroom] fatal error (code ${code}): ${reason}`);
161
+ client.onFatal((reason) => {
162
+ log?.error?.(`[clawroom] fatal error: ${reason}`);
190
163
  ctx.setStatus({
191
164
  accountId: account.accountId,
192
165
  running: false,
@@ -1,6 +1,5 @@
1
- import type { ServerChatMessage } from "@clawroom/sdk";
2
1
  import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
- import type { ClawroomClient } from "./client.js";
2
+ import type { ClawroomPluginClient } from "./client.js";
4
3
 
5
4
  /** Default timeout for chat reply subagent (2 minutes). */
6
5
  const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
@@ -12,7 +11,7 @@ const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
12
11
  * 3. Send reply back to channel
13
12
  */
14
13
  export function setupChatExecutor(opts: {
15
- client: ClawroomClient;
14
+ client: ClawroomPluginClient;
16
15
  runtime: PluginRuntime;
17
16
  log?: {
18
17
  info?: (message: string, ...args: unknown[]) => void;
@@ -22,61 +21,70 @@ export function setupChatExecutor(opts: {
22
21
  }): void {
23
22
  const { client, runtime, log } = opts;
24
23
 
25
- client.onChatMessage((messages: ServerChatMessage[]) => {
24
+ client.onAgentChat((agentId: string, messages: any[]) => {
26
25
  for (const msg of messages) {
27
- void handleChatMention({ client, runtime, msg, log });
26
+ void handleChatMention({ client, runtime, msg, agentId, log });
28
27
  }
29
28
  });
30
29
  }
31
30
 
32
31
  async function handleChatMention(opts: {
33
- client: ClawroomClient;
32
+ client: ClawroomPluginClient;
34
33
  runtime: PluginRuntime;
35
- msg: ServerChatMessage;
34
+ msg: any;
35
+ agentId: string;
36
36
  log?: {
37
37
  info?: (message: string, ...args: unknown[]) => void;
38
38
  warn?: (message: string, ...args: unknown[]) => void;
39
39
  error?: (message: string, ...args: unknown[]) => void;
40
40
  };
41
41
  }): Promise<void> {
42
- const { client, runtime, msg, log } = opts;
43
- const sessionKey = `clawroom:chat:${msg.channelId}:${msg.messageId}`;
42
+ const { client, runtime, msg, agentId, log } = opts;
43
+ const sessionKey = `clawroom:chat:${agentId}:${msg.channelId}:${msg.messageId}`;
44
44
 
45
45
  log?.info?.(`[clawroom:chat] processing mention in channel ${msg.channelId}: "${msg.content.slice(0, 80)}"`);
46
46
 
47
47
  // Send typing indicator
48
- client.send({ type: "agent.typing", channelId: msg.channelId });
48
+ client.sendTyping(agentId, msg.channelId).catch(() => {});
49
49
 
50
50
  // Build context from recent messages
51
51
  const contextLines = msg.context
52
52
  .map((c) => `[${c.senderName}]: ${c.content}`)
53
53
  .join("\n");
54
54
 
55
+ const isMention = (msg as any).isMention ?? false;
55
56
  const agentMessage = [
56
- "You were mentioned in a chat channel. Reply concisely and helpfully.",
57
+ isMention
58
+ ? "You were @mentioned in a chat channel. Reply directly and helpfully."
59
+ : "A new message appeared in a channel you're in. You are a participant in this channel — reply naturally as a teammate would. If the message is a greeting or directed at the group, respond. Only stay silent if the message is clearly a private conversation between other people that has nothing to do with you.",
57
60
  "",
58
61
  "## Recent messages",
59
62
  contextLines,
60
63
  "",
61
- "## Message that mentioned you",
64
+ isMention ? "## Message that mentioned you" : "## Latest message",
62
65
  msg.content,
63
66
  ].join("\n");
64
67
 
65
68
  try {
66
69
  // Keep typing indicator alive during execution
67
70
  const typingInterval = setInterval(() => {
68
- client.send({ type: "agent.typing", channelId: msg.channelId });
71
+ client.sendTyping(agentId, msg.channelId).catch(() => {});
69
72
  }, 3000);
70
73
 
71
74
  const { runId } = await runtime.subagent.run({
72
75
  sessionKey,
73
76
  idempotencyKey: `clawroom:chat:${msg.messageId}`,
74
77
  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.",
78
+ extraSystemPrompt: isMention
79
+ ? "You are responding to a direct @mention in ClawRoom. " +
80
+ "You MUST reply with a helpful response. " +
81
+ "Keep your reply SHORT and conversational (1-3 sentences). " +
82
+ "Do NOT include file paths, system info, or markdown headers."
83
+ : "You are a member of a chat channel in ClawRoom. " +
84
+ "Respond naturally as a teammate. If someone greets the channel, greet back. " +
85
+ "If someone asks a question, help if you can. " +
86
+ "Only respond with exactly SKIP if the message is clearly a private exchange between others that doesn't involve you at all. " +
87
+ "Keep replies SHORT (1-3 sentences).",
80
88
  lane: "clawroom",
81
89
  });
82
90
 
@@ -89,23 +97,13 @@ async function handleChatMention(opts: {
89
97
 
90
98
  if (waitResult.status === "error") {
91
99
  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
- });
100
+ await client.sendChatReply(agentId, msg.channelId, "Sorry, I encountered an error processing your message.");
98
101
  return;
99
102
  }
100
103
 
101
104
  if (waitResult.status === "timeout") {
102
105
  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
- });
106
+ await client.sendChatReply(agentId, msg.channelId, "Sorry, I took too long to respond. Please try again.");
109
107
  return;
110
108
  }
111
109
 
@@ -116,13 +114,15 @@ async function handleChatMention(opts: {
116
114
 
117
115
  const reply = extractLastAssistantMessage(messages);
118
116
 
117
+ // If agent chose to skip (awareness message, not relevant), don't reply
118
+ if (reply.trim() === "SKIP" || reply.trim() === "skip") {
119
+ log?.info?.(`[clawroom:chat] skipping ${msg.messageId} (not relevant)`);
120
+ await runtime.subagent.deleteSession({ sessionKey, deleteTranscript: true });
121
+ return;
122
+ }
123
+
119
124
  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
- });
125
+ await client.sendChatReply(agentId, msg.channelId, reply);
126
126
 
127
127
  await runtime.subagent.deleteSession({
128
128
  sessionKey,
@@ -131,12 +131,7 @@ async function handleChatMention(opts: {
131
131
  } catch (err) {
132
132
  const reason = err instanceof Error ? err.message : String(err);
133
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
- });
134
+ await client.sendChatReply(agentId, msg.channelId, "Sorry, something went wrong. Please try again.");
140
135
  }
141
136
  }
142
137
 
package/src/client.ts CHANGED
@@ -1,63 +1,56 @@
1
- import { ClawroomClient as BaseClient } from "@clawroom/sdk";
2
- import type { ClawroomClientOptions } from "@clawroom/sdk";
1
+ import { ClawroomMachineClient } from "@clawroom/sdk";
2
+ import type { ClawroomMachineClientOptions } from "@clawroom/sdk";
3
3
 
4
4
  type DisconnectCallback = () => void;
5
- type WelcomeCallback = (agentId: string) => void;
6
- type FatalCallback = (reason: string, code?: number) => void;
5
+ type WelcomeCallback = (machineId: string) => void;
6
+ type FatalCallback = (reason: string) => void;
7
7
 
8
8
  /**
9
- * Plugin-specific ClawRoom client that extends the SDK client
10
- * with OpenClaw lifecycle hooks (connected state, disconnect, fatal).
9
+ * Plugin-specific ClawRoom machine client.
10
+ * Wraps ClawroomMachineClient with OpenClaw lifecycle hooks.
11
11
  */
12
- export class ClawroomClient extends BaseClient {
13
- private connected = false;
12
+ export class ClawroomPluginClient {
13
+ private inner: ClawroomMachineClient;
14
14
  private disconnectCallbacks: DisconnectCallback[] = [];
15
15
  private welcomeCallbacks: WelcomeCallback[] = [];
16
16
  private fatalCallbacks: FatalCallback[] = [];
17
17
 
18
- constructor(options: ClawroomClientOptions) {
19
- super(options);
18
+ constructor(options: ClawroomMachineClientOptions) {
19
+ this.inner = new ClawroomMachineClient(options);
20
20
  }
21
21
 
22
- get isAlive(): boolean { return !this.stopped; }
23
- get isConnected(): boolean { return this.connected; }
22
+ get isAlive(): boolean { return !this.inner.stopped; }
23
+ get isConnected(): boolean { return this.inner.connected; }
24
24
 
25
25
  onDisconnect(cb: DisconnectCallback): void { this.disconnectCallbacks.push(cb); }
26
26
  onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
27
27
  onFatal(cb: FatalCallback): void { this.fatalCallbacks.push(cb); }
28
28
 
29
- override disconnect(): void {
30
- super.disconnect();
31
- this.markDisconnected();
29
+ onAgentTask(handler: (agentId: string, task: any) => void) { this.inner.onAgentTask(handler); }
30
+ onAgentChat(handler: (agentId: string, messages: any[]) => void) { this.inner.onAgentChat(handler); }
31
+
32
+ async sendComplete(agentId: string, taskId: string, output: string, attachments?: Array<{ filename: string; mimeType: string; data: string }>) {
33
+ await this.inner.sendAgentComplete(agentId, taskId, output);
34
+ }
35
+
36
+ async sendFail(agentId: string, taskId: string, reason: string) {
37
+ await this.inner.sendAgentFail(agentId, taskId, reason);
32
38
  }
33
39
 
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
- }
40
+ async sendChatReply(agentId: string, channelId: string, content: string) {
41
+ await this.inner.sendAgentChatReply(agentId, channelId, content);
42
42
  }
43
43
 
44
- protected override onPollError(_err: unknown): void {
45
- this.markDisconnected();
44
+ async sendTyping(agentId: string, channelId: string) {
45
+ await this.inner.sendAgentTyping(agentId, channelId);
46
46
  }
47
47
 
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);
53
- }
48
+ connect(): void {
49
+ this.inner.connect();
54
50
  }
55
51
 
56
- private markDisconnected(): void {
57
- if (!this.connected) return;
58
- this.connected = false;
52
+ disconnect(): void {
53
+ this.inner.disconnect();
59
54
  for (const cb of this.disconnectCallbacks) cb();
60
55
  }
61
56
  }
62
-
63
- export type { ClawroomClientOptions };
@@ -1,208 +1,99 @@
1
- import type { ServerTask, AgentResultFile } from "@clawroom/sdk";
1
+ import type { AgentResultFile } from "@clawroom/sdk";
2
2
  import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
- import type { ClawroomClient } from "./client.js";
3
+ import type { ClawroomPluginClient } from "./client.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
 
7
- /** Default timeout for waiting on a subagent run (5 minutes). */
8
7
  const SUBAGENT_TIMEOUT_MS = 5 * 60 * 1000;
9
-
10
- /** Max file size to upload (10 MB). */
11
8
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
12
9
 
13
- /**
14
- * Wire up the task execution pipeline.
15
- *
16
- * Tasks are assigned by humans and delivered via /poll.
17
- * On receiving an assigned task, execute it immediately via subagent.
18
- */
19
10
  export function setupTaskExecutor(opts: {
20
- client: ClawroomClient;
11
+ client: ClawroomPluginClient;
21
12
  runtime: PluginRuntime;
22
- log?: {
23
- info?: (message: string, ...args: unknown[]) => void;
24
- warn?: (message: string, ...args: unknown[]) => void;
25
- error?: (message: string, ...args: unknown[]) => void;
26
- };
13
+ log?: { info?: (m: string, ...a: unknown[]) => void; warn?: (m: string, ...a: unknown[]) => void; error?: (m: string, ...a: unknown[]) => void };
27
14
  }): void {
28
15
  const { client, runtime, log } = opts;
29
16
  const activeTasks = new Set<string>();
30
17
 
31
- client.onTask((task: ServerTask) => {
32
- if (activeTasks.has(task.taskId)) {
33
- log?.info?.(`[clawroom:executor] task ${task.taskId} already running, skipping`);
34
- return;
35
- }
36
-
37
- log?.info?.(`[clawroom:executor] executing assigned task ${task.taskId}: ${task.title}`);
38
- activeTasks.add(task.taskId);
39
- void executeTask({ client, runtime, task, log }).finally(() => {
40
- activeTasks.delete(task.taskId);
41
- });
18
+ client.onAgentTask((agentId: string, task: any) => {
19
+ const key = `${agentId}:${task.taskId}`;
20
+ if (activeTasks.has(key)) return;
21
+ activeTasks.add(key);
22
+ log?.info?.(`[clawroom:executor] [${agentId}] executing task ${task.taskId}: ${task.title}`);
23
+ void executeTask({ client, runtime, task, agentId, log }).finally(() => activeTasks.delete(key));
42
24
  });
43
25
  }
44
26
 
45
- /**
46
- * Execute a claimed task by running a subagent session and reporting the
47
- * result back to the ClawRoom server.
48
- */
49
27
  async function executeTask(opts: {
50
- client: ClawroomClient;
28
+ client: ClawroomPluginClient;
51
29
  runtime: PluginRuntime;
52
- task: ServerTask;
53
- log?: {
54
- info?: (message: string, ...args: unknown[]) => void;
55
- error?: (message: string, ...args: unknown[]) => void;
56
- };
30
+ task: any;
31
+ agentId: string;
32
+ log?: { info?: (m: string, ...a: unknown[]) => void; error?: (m: string, ...a: unknown[]) => void };
57
33
  }): Promise<void> {
58
- const { client, runtime, task, log } = opts;
59
- const sessionKey = `clawroom:task:${task.taskId}`;
34
+ const { client, runtime, task, agentId, log } = opts;
35
+ const sessionKey = `clawroom:task:${agentId}:${task.taskId}`;
60
36
 
61
- const agentMessage = buildAgentMessage(task);
37
+ const parts: string[] = [`# Task: ${task.title}`];
38
+ if (task.description) parts.push("", task.description);
39
+ if (task.input) parts.push("", "## Input", "", task.input);
40
+ if (task.skillTags?.length) parts.push("", `Skills: ${task.skillTags.join(", ")}`);
41
+ const agentMessage = parts.join("\n");
62
42
 
63
43
  try {
64
- log?.info?.(`[clawroom:executor] running subagent for task ${task.taskId}`);
65
-
66
44
  const { runId } = await runtime.subagent.run({
67
45
  sessionKey,
68
- idempotencyKey: `clawroom:${task.taskId}`,
46
+ idempotencyKey: `clawroom:${agentId}:${task.taskId}`,
69
47
  message: agentMessage,
70
48
  extraSystemPrompt:
71
- "You are executing a task from the ClawRoom marketplace. " +
72
- "Complete the task and provide a SHORT summary (2-3 sentences) of what you did. " +
73
- "Do NOT include any local file paths, machine info, or internal details in your summary. " +
74
- "If you create output files, list their absolute paths at the very end, " +
75
- "one per line, prefixed with 'OUTPUT_FILE: '. These markers will be processed " +
76
- "automatically and stripped before showing to the user. " +
77
- "Example: OUTPUT_FILE: /path/to/report.pdf",
49
+ "You are executing a task from ClawRoom. Complete it and provide a SHORT summary (2-3 sentences). " +
50
+ "Do NOT include local file paths or internal details. " +
51
+ "If you create output files, list them at the end prefixed with 'OUTPUT_FILE: /path'.",
78
52
  lane: "clawroom",
79
53
  });
80
54
 
81
- const waitResult = await runtime.subagent.waitForRun({
82
- runId,
83
- timeoutMs: SUBAGENT_TIMEOUT_MS,
84
- });
55
+ const waitResult = await runtime.subagent.waitForRun({ runId, timeoutMs: SUBAGENT_TIMEOUT_MS });
85
56
 
86
57
  if (waitResult.status === "error") {
87
- log?.error?.(
88
- `[clawroom:executor] subagent error for task ${task.taskId}: ${waitResult.error}`,
89
- );
90
- client.send({
91
- type: "agent.fail",
92
- taskId: task.taskId,
93
- reason: waitResult.error ?? "Agent execution failed",
94
- });
58
+ log?.error?.(`[clawroom:executor] [${agentId}] subagent error: ${waitResult.error}`);
59
+ await client.sendFail(agentId, task.taskId, waitResult.error ?? "Agent execution failed");
95
60
  return;
96
61
  }
97
-
98
62
  if (waitResult.status === "timeout") {
99
- log?.error?.(`[clawroom:executor] subagent timeout for task ${task.taskId}`);
100
- client.send({
101
- type: "agent.fail",
102
- taskId: task.taskId,
103
- reason: "Agent execution timed out",
104
- });
63
+ log?.error?.(`[clawroom:executor] [${agentId}] subagent timeout`);
64
+ await client.sendFail(agentId, task.taskId, "Agent execution timed out");
105
65
  return;
106
66
  }
107
67
 
108
- const { messages } = await runtime.subagent.getSessionMessages({
109
- sessionKey,
110
- limit: 100,
111
- });
112
-
68
+ const { messages } = await runtime.subagent.getSessionMessages({ sessionKey, limit: 100 });
113
69
  const rawOutput = extractAgentOutput(messages);
114
- const filesFromTools = extractWrittenFiles(messages);
115
- const filesFromOutput = extractOutputFileMarkers(rawOutput);
116
- const allFiles = [...new Set([...filesFromTools, ...filesFromOutput])];
70
+ const allFiles = [...new Set([...extractWrittenFiles(messages), ...extractOutputFileMarkers(rawOutput)])];
117
71
  const files = readAllFiles(allFiles, log);
72
+ const output = rawOutput.split("\n").filter((l) => !l.match(/OUTPUT_FILE:\s*/)).join("\n").replace(/`?\/\S+\/[^\s`]+`?/g, "").replace(/\n{3,}/g, "\n\n").trim();
118
73
 
119
- // Strip OUTPUT_FILE markers and internal paths from the user-facing output
120
- const output = rawOutput
121
- .split("\n")
122
- .filter((line) => !line.match(/OUTPUT_FILE:\s*/))
123
- .join("\n")
124
- .replace(/`?\/\S+\/[^\s`]+`?/g, "") // strip absolute paths
125
- .replace(/\n{3,}/g, "\n\n") // collapse extra newlines
126
- .trim();
127
-
128
- log?.info?.(`[clawroom:executor] task ${task.taskId} completed${files.length > 0 ? ` (with ${files.length} file(s))` : ""}`);
129
- client.send({
130
- type: "agent.complete",
131
- taskId: task.taskId,
132
- output,
133
- attachments: files.length > 0 ? files : undefined,
134
- });
135
-
136
- await runtime.subagent.deleteSession({
137
- sessionKey,
138
- deleteTranscript: true,
139
- });
74
+ log?.info?.(`[clawroom:executor] [${agentId}] task completed${files.length > 0 ? ` (${files.length} files)` : ""}`);
75
+ await client.sendComplete(agentId, task.taskId, output, files.length > 0 ? files : undefined);
76
+ await runtime.subagent.deleteSession({ sessionKey, deleteTranscript: true });
140
77
  } catch (err) {
141
78
  const reason = err instanceof Error ? err.message : String(err);
142
- log?.error?.(`[clawroom:executor] unexpected error for task ${task.taskId}: ${reason}`);
143
- client.send({
144
- type: "agent.fail",
145
- taskId: task.taskId,
146
- reason,
147
- });
148
- }
149
- }
150
-
151
- function buildAgentMessage(task: ServerTask): string {
152
- const parts: string[] = [];
153
- parts.push(`# Task: ${task.title}`);
154
- if (task.description) {
155
- parts.push("", task.description);
156
- }
157
- if (task.input) {
158
- parts.push("", "## Input", "", task.input);
159
- }
160
- if (task.skillTags.length > 0) {
161
- parts.push("", `Skills: ${task.skillTags.join(", ")}`);
79
+ log?.error?.(`[clawroom:executor] [${agentId}] error: ${reason}`);
80
+ await client.sendFail(agentId, task.taskId, reason);
162
81
  }
163
- return parts.join("\n");
164
82
  }
165
83
 
166
84
  function extractAgentOutput(messages: unknown[]): string {
167
85
  for (let i = messages.length - 1; i >= 0; i--) {
168
86
  const msg = messages[i] as Record<string, unknown> | undefined;
169
- if (!msg) continue;
170
-
171
- if (msg.role === "assistant") {
172
- if (typeof msg.content === "string") {
173
- return msg.content;
174
- }
175
- if (Array.isArray(msg.content)) {
176
- const textParts: string[] = [];
177
- for (const block of msg.content) {
178
- const b = block as Record<string, unknown>;
179
- if (b.type === "text" && typeof b.text === "string") {
180
- textParts.push(b.text);
181
- }
182
- }
183
- if (textParts.length > 0) {
184
- return textParts.join("\n");
185
- }
186
- }
87
+ if (!msg || msg.role !== "assistant") continue;
88
+ if (typeof msg.content === "string") return msg.content;
89
+ if (Array.isArray(msg.content)) {
90
+ const parts = (msg.content as Record<string, unknown>[]).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text as string);
91
+ if (parts.length > 0) return parts.join("\n");
187
92
  }
188
93
  }
189
-
190
94
  return "(No output produced)";
191
95
  }
192
96
 
193
- const MIME_MAP: Record<string, string> = {
194
- ".md": "text/markdown", ".txt": "text/plain", ".pdf": "application/pdf",
195
- ".json": "application/json", ".csv": "text/csv", ".html": "text/html",
196
- ".js": "application/javascript", ".ts": "application/typescript",
197
- ".py": "text/x-python", ".png": "image/png", ".jpg": "image/jpeg",
198
- ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml",
199
- ".mp4": "video/mp4", ".zip": "application/zip",
200
- };
201
-
202
- /**
203
- * Extract file paths from agent session messages by looking at Write/write_file
204
- * tool calls in the assistant's content blocks.
205
- */
206
97
  function extractWrittenFiles(messages: unknown[]): string[] {
207
98
  const files = new Set<string>();
208
99
  for (const msg of messages) {
@@ -210,14 +101,13 @@ function extractWrittenFiles(messages: unknown[]): string[] {
210
101
  if (!m || !Array.isArray(m.content)) continue;
211
102
  for (const block of m.content) {
212
103
  const b = block as Record<string, unknown>;
213
- // Match tool_use blocks (Claude-style) and tool_call blocks (OpenAI-style)
214
104
  if (b.type !== "tool_use" && b.type !== "tool_call") continue;
215
105
  const name = String(b.name ?? b.function ?? "").toLowerCase();
216
106
  if (!name.includes("write") && !name.includes("create_file") && !name.includes("save")) continue;
217
107
  const raw = b.input ?? b.arguments;
218
- const input = typeof raw === "string" ? tryParse(raw) : raw;
108
+ const input = typeof raw === "string" ? (() => { try { return JSON.parse(raw); } catch { return null; } })() : raw;
219
109
  if (input && typeof input === "object") {
220
- const fp = (input as Record<string, unknown>).file_path ?? (input as Record<string, unknown>).path;
110
+ const fp = (input as any).file_path ?? (input as any).path;
221
111
  if (typeof fp === "string" && fp) files.add(fp);
222
112
  }
223
113
  }
@@ -225,53 +115,28 @@ function extractWrittenFiles(messages: unknown[]): string[] {
225
115
  return Array.from(files);
226
116
  }
227
117
 
228
- /**
229
- * Extract file paths from OUTPUT_FILE: markers in the agent's text output.
230
- */
231
118
  function extractOutputFileMarkers(output: string): string[] {
232
- const files: string[] = [];
233
- for (const line of output.split("\n")) {
234
- const match = line.match(/OUTPUT_FILE:\s*(.+)/);
235
- if (match) {
236
- const fp = match[1].trim().replace(/`/g, "");
237
- if (fp) files.push(fp);
238
- }
239
- }
240
- return files;
119
+ return output.split("\n").map((l) => l.match(/OUTPUT_FILE:\s*(.+)/)?.[1]?.trim().replace(/`/g, "")).filter((f): f is string => !!f);
241
120
  }
242
121
 
243
- function tryParse(s: string): unknown {
244
- try { return JSON.parse(s); } catch { return null; }
245
- }
122
+ const MIME_MAP: Record<string, string> = {
123
+ ".md": "text/markdown", ".txt": "text/plain", ".pdf": "application/pdf",
124
+ ".json": "application/json", ".csv": "text/csv", ".html": "text/html",
125
+ ".js": "application/javascript", ".ts": "application/typescript",
126
+ ".py": "text/x-python", ".png": "image/png", ".jpg": "image/jpeg",
127
+ ".gif": "image/gif", ".svg": "image/svg+xml", ".zip": "application/zip",
128
+ };
246
129
 
247
- /**
248
- * Read all valid files from disk and encode them as base64.
249
- * Skips missing files and files > MAX_FILE_SIZE.
250
- */
251
- function readAllFiles(
252
- paths: string[],
253
- log?: {
254
- info?: (message: string, ...args: unknown[]) => void;
255
- warn?: (message: string, ...args: unknown[]) => void;
256
- },
257
- ): AgentResultFile[] {
130
+ function readAllFiles(paths: string[], log?: { warn?: (m: string, ...a: unknown[]) => void; info?: (m: string, ...a: unknown[]) => void }): AgentResultFile[] {
258
131
  const results: AgentResultFile[] = [];
259
132
  for (const fp of paths) {
260
133
  try {
261
- if (!fs.existsSync(fp)) { log?.warn?.(`[clawroom:executor] file not found: ${fp}`); continue; }
134
+ if (!fs.existsSync(fp)) continue;
262
135
  const stat = fs.statSync(fp);
263
- if (stat.size > MAX_FILE_SIZE) { log?.warn?.(`[clawroom:executor] file too large: ${fp} (${stat.size})`); continue; }
136
+ if (stat.size > MAX_FILE_SIZE) continue;
264
137
  const data = fs.readFileSync(fp);
265
- const ext = path.extname(fp).toLowerCase();
266
- log?.info?.(`[clawroom:executor] attached: ${path.basename(fp)} (${data.length} bytes)`);
267
- results.push({
268
- filename: path.basename(fp),
269
- mimeType: MIME_MAP[ext] ?? "application/octet-stream",
270
- data: data.toString("base64"),
271
- });
272
- } catch (err) {
273
- log?.warn?.(`[clawroom:executor] failed to read ${fp}: ${err}`);
274
- }
138
+ results.push({ filename: path.basename(fp), mimeType: MIME_MAP[path.extname(fp).toLowerCase()] ?? "application/octet-stream", data: data.toString("base64") });
139
+ } catch {}
275
140
  }
276
141
  return results;
277
142
  }