@clawroom/sdk 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawroom/sdk",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "ClawRoom SDK — polling client and protocol types for connecting any agent to ClawRoom",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AgentChatProfile,
2
3
  AgentMessage,
3
4
  ServerTask,
4
5
  ServerChatMessage,
@@ -8,6 +9,12 @@ import { WsTransport } from "./ws-transport.js";
8
9
  const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
9
10
  const HEARTBEAT_INTERVAL_MS = 30_000;
10
11
  const POLL_INTERVAL_MS = 10_000;
12
+ const DEFAULT_AGENT_CHAT_PROFILE: AgentChatProfile = {
13
+ role: "No role defined",
14
+ systemPrompt: "No system prompt configured",
15
+ memory: "No memory recorded yet",
16
+ focus: "No focus items",
17
+ };
11
18
 
12
19
  export type TaskCallback = (task: ServerTask) => void;
13
20
  export type ChatCallback = (messages: ServerChatMessage[]) => void;
@@ -39,6 +46,7 @@ export class ClawroomClient {
39
46
  protected taskCallbacks: TaskCallback[] = [];
40
47
  protected chatCallbacks: ChatCallback[] = [];
41
48
  private wsTransport: WsTransport | null = null;
49
+ private recentChatIds = new Set<string>();
42
50
 
43
51
  constructor(options: ClawroomClientOptions) {
44
52
  this.options = options;
@@ -67,18 +75,40 @@ export class ClawroomClient {
67
75
  for (const cb of this.taskCallbacks) cb(msg.task);
68
76
  }
69
77
  if (msg.type === "chat" && Array.isArray(msg.messages)) {
70
- for (const cb of this.chatCallbacks) cb(msg.messages);
78
+ const fresh = msg.messages.filter((m: ServerChatMessage) => this.rememberChat(m.messageId));
79
+ if (fresh.length > 0) {
80
+ for (const cb of this.chatCallbacks) cb(fresh);
81
+ this.ackChatBestEffort(fresh.map((m: ServerChatMessage) => m.messageId));
82
+ }
83
+ }
84
+ if (msg.type === "message" && msg.message) {
85
+ const agentProfile = msg.agentProfile ?? msg.message.agentProfile ?? DEFAULT_AGENT_CHAT_PROFILE;
86
+ const delivered = [{
87
+ messageId: msg.message.id ?? msg.messageId,
88
+ channelId: msg.message.channelId ?? "",
89
+ content: msg.message.content ?? "",
90
+ context: Array.isArray(msg.context) ? msg.context : [],
91
+ isMention: msg.isMention ?? false,
92
+ agentProfile,
93
+ }] as ServerChatMessage[];
94
+ const fresh = delivered.filter((m) => this.rememberChat(m.messageId));
95
+ if (fresh.length > 0) {
96
+ for (const cb of this.chatCallbacks) cb(fresh);
97
+ this.ackChatBestEffort(fresh.map((m) => m.messageId));
98
+ }
71
99
  }
72
100
  },
73
101
  });
74
102
  this.wsTransport.connect();
75
103
 
76
- // HTTP poll fallback only when WS is down
104
+ // Keep HTTP polling active even when WS is up.
105
+ // WS is the fast path; polling is the durable delivery path.
77
106
  this.stopPolling();
78
107
  this.pollTimer = setInterval(() => {
79
- if (this.wsTransport?.connected) return;
80
108
  void this.pollTick();
81
109
  }, POLL_INTERVAL_MS);
110
+
111
+ void this.pollTick();
82
112
  }
83
113
 
84
114
  disconnect(): void {
@@ -137,8 +167,12 @@ export class ClawroomClient {
137
167
  for (const cb of this.taskCallbacks) cb(res.task);
138
168
  }
139
169
  if (res.chat && Array.isArray(res.chat) && res.chat.length > 0) {
140
- this.options.log?.info?.(`[clawroom] received ${res.chat.length} chat mention(s)`);
141
- for (const cb of this.chatCallbacks) cb(res.chat);
170
+ const fresh = res.chat.filter((m: ServerChatMessage) => this.rememberChat(m.messageId));
171
+ if (fresh.length > 0) {
172
+ this.options.log?.info?.(`[clawroom] received ${fresh.length} chat mention(s)`);
173
+ for (const cb of this.chatCallbacks) cb(fresh);
174
+ this.ackChatBestEffort(fresh.map((m: ServerChatMessage) => m.messageId));
175
+ }
142
176
  }
143
177
  } catch (err) {
144
178
  this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
@@ -162,7 +196,7 @@ export class ClawroomClient {
162
196
  }
163
197
  }
164
198
 
165
- protected async httpRequest(method: string, path: string, body: unknown): Promise<any> {
199
+ protected async httpRequest(method: string, path: string, body: unknown): Promise<unknown> {
166
200
  const res = await fetch(`${this.httpBase}${path}`, {
167
201
  method,
168
202
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
@@ -177,4 +211,27 @@ export class ClawroomClient {
177
211
  }
178
212
 
179
213
  protected onHttpError(_status: number, _text: string): void {}
214
+
215
+ private rememberChat(messageId?: string): boolean {
216
+ if (!messageId) return true;
217
+ if (this.recentChatIds.has(messageId)) return false;
218
+ this.recentChatIds.add(messageId);
219
+ if (this.recentChatIds.size > 1000) {
220
+ const oldest = this.recentChatIds.values().next().value;
221
+ if (oldest) this.recentChatIds.delete(oldest);
222
+ }
223
+ return true;
224
+ }
225
+
226
+ private ackChatBestEffort(messageIds: string[]): void {
227
+ void this.ackChat(messageIds).catch((err) => {
228
+ this.options.log?.warn?.(`[clawroom] chat ack error: ${err}`);
229
+ });
230
+ }
231
+
232
+ private async ackChat(messageIds: string[]): Promise<void> {
233
+ const ids = Array.from(new Set(messageIds.filter(Boolean)));
234
+ if (ids.length === 0) return;
235
+ await this.httpRequest("POST", "/chat/ack", { messageIds: ids });
236
+ }
180
237
  }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { WsTransport } from "./ws-transport.js";
6
6
  export type { WsTransportOptions } from "./ws-transport.js";
7
7
 
8
8
  export type {
9
+ AgentChatProfile,
9
10
  AgentMessage,
10
11
  AgentHeartbeat,
11
12
  AgentComplete,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as os from "node:os";
7
+ import type { AgentChatProfile, ServerChatMessage } from "./protocol.js";
7
8
  import { WsTransport } from "./ws-transport.js";
8
9
 
9
10
  export type ClawroomMachineClientOptions = {
@@ -23,7 +24,14 @@ type AgentWork = {
23
24
  agentId: string;
24
25
  agentName: string;
25
26
  task: { type: "server.task"; taskId: string; title: string; description: string; input: string; skillTags: string[] } | null;
26
- chat: Array<{ messageId: string; channelId: string; content: string; isMention: boolean; context: unknown[] }> | null;
27
+ chat: ServerChatMessage[] | null;
28
+ };
29
+
30
+ const DEFAULT_AGENT_CHAT_PROFILE: AgentChatProfile = {
31
+ role: "No role defined",
32
+ systemPrompt: "No system prompt configured",
33
+ memory: "No memory recorded yet",
34
+ focus: "No focus items",
27
35
  };
28
36
 
29
37
  type MachineHeartbeatResponse = {
@@ -42,6 +50,8 @@ export class ClawroomMachineClient {
42
50
  private pollTimer: ReturnType<typeof setInterval> | null = null;
43
51
  private taskHandler: ((agentId: string, task: AgentWork["task"]) => void) | null = null;
44
52
  private chatHandler: ((agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) | null = null;
53
+ private connectHandler: ((machineId: string) => void) | null = null;
54
+ private disconnectHandler: (() => void) | null = null;
45
55
  private _connected = false;
46
56
  private _stopped = false;
47
57
  private wsTransport: WsTransport | null = null;
@@ -74,6 +84,8 @@ export class ClawroomMachineClient {
74
84
 
75
85
  onAgentTask(handler: (agentId: string, task: AgentWork["task"]) => void) { this.taskHandler = handler; }
76
86
  onAgentChat(handler: (agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) { this.chatHandler = handler; }
87
+ onConnected(handler: (machineId: string) => void) { this.connectHandler = handler; }
88
+ onDisconnected(handler: () => void) { this.disconnectHandler = handler; }
77
89
 
78
90
  async sendAgentComplete(agentId: string, taskId: string, output: string, attachments?: Array<{ filename: string; mimeType: string; data: string }>) {
79
91
  await this.httpRequest("POST", "/complete", { agentId, taskId, output, attachments });
@@ -91,6 +103,12 @@ export class ClawroomMachineClient {
91
103
  await this.httpRequest("POST", "/typing", { agentId, channelId });
92
104
  }
93
105
 
106
+ async ackAgentChat(agentId: string, messageIds: string[]) {
107
+ const ids = Array.from(new Set(messageIds.filter(Boolean)));
108
+ if (ids.length === 0) return;
109
+ await this.httpRequest("POST", "/chat-ack", { agentId, messageIds: ids });
110
+ }
111
+
94
112
  get connected() { return this._connected; }
95
113
  get stopped() { return this._stopped; }
96
114
 
@@ -105,6 +123,12 @@ export class ClawroomMachineClient {
105
123
  return true;
106
124
  }
107
125
 
126
+ private ackAgentChatBestEffort(agentId: string, messageIds: string[]) {
127
+ void this.ackAgentChat(agentId, messageIds).catch((err) => {
128
+ this.options.log?.warn?.(`[machine] chat ack error: ${err}`);
129
+ });
130
+ }
131
+
108
132
  private syncAgentSubscriptions(agentIds: string[]) {
109
133
  if (agentIds.length === 0) return;
110
134
  for (const agentId of agentIds) {
@@ -113,6 +137,22 @@ export class ClawroomMachineClient {
113
137
  }
114
138
  }
115
139
 
140
+ private markConnected(machineId: string) {
141
+ if (!this._connected) {
142
+ this._connected = true;
143
+ this.options.log?.info?.("[machine] connected");
144
+ this.connectHandler?.(machineId);
145
+ }
146
+ }
147
+
148
+ private markDisconnected() {
149
+ if (this._connected) {
150
+ this._connected = false;
151
+ this.options.log?.warn?.("[machine] disconnected");
152
+ this.disconnectHandler?.();
153
+ }
154
+ }
155
+
116
156
  private async pollOnce() {
117
157
  if (!this._connected) return;
118
158
  try {
@@ -122,7 +162,10 @@ export class ClawroomMachineClient {
122
162
  if (agent.task && this.taskHandler) this.taskHandler(agent.agentId, agent.task);
123
163
  if (agent.chat && agent.chat.length > 0 && this.chatHandler) {
124
164
  const freshMessages = agent.chat.filter((message) => this.rememberChat(message.messageId));
125
- if (freshMessages.length > 0) this.chatHandler(agent.agentId, freshMessages);
165
+ if (freshMessages.length > 0) {
166
+ this.chatHandler(agent.agentId, freshMessages);
167
+ this.ackAgentChatBestEffort(agent.agentId, freshMessages.map((message) => message.messageId));
168
+ }
126
169
  }
127
170
  }
128
171
  } catch (err) {
@@ -140,18 +183,17 @@ export class ClawroomMachineClient {
140
183
  try {
141
184
  const result = (await this.httpRequest("POST", "/heartbeat", hbBody)) as MachineHeartbeatResponse;
142
185
  this.syncAgentSubscriptions((result.agents ?? []).map((agent) => agent.id));
143
- if (!this._connected) { this._connected = true; this.options.log?.info?.("[machine] connected"); }
186
+ this.markConnected(result.machineId);
144
187
  } catch {
145
- if (this._connected) { this._connected = false; this.options.log?.warn?.("[machine] disconnected"); }
188
+ this.markDisconnected();
146
189
  }
147
190
  }, 30_000);
148
191
 
149
192
  this.httpRequest("POST", "/heartbeat", hbBody)
150
193
  .then((result) => {
151
194
  const data = result as MachineHeartbeatResponse;
152
- this._connected = true;
153
195
  this.syncAgentSubscriptions((data.agents ?? []).map((agent) => agent.id));
154
- this.options.log?.info?.("[machine] connected");
196
+ this.markConnected(data.machineId);
155
197
  })
156
198
  .catch((err) => this.options.log?.warn?.(`[machine] initial heartbeat failed: ${err}`));
157
199
 
@@ -173,13 +215,17 @@ export class ClawroomMachineClient {
173
215
  const targetAgentId = msg.agentId ?? m?.agentId;
174
216
  const messageId = m?.id ?? msg.messageId ?? "";
175
217
  if (m && targetAgentId && this.chatHandler && this.rememberChat(messageId)) {
176
- this.chatHandler(targetAgentId, [{
218
+ const agentProfile = msg.agentProfile ?? m.agentProfile ?? DEFAULT_AGENT_CHAT_PROFILE;
219
+ const messages = [{
177
220
  messageId,
178
221
  channelId: m.channelId ?? "",
179
222
  content: m.content ?? "",
180
223
  isMention: msg.isMention ?? false,
181
224
  context: msg.context ?? [],
182
- }]);
225
+ agentProfile,
226
+ }];
227
+ this.chatHandler(targetAgentId, messages);
228
+ this.ackAgentChatBestEffort(targetAgentId, messages.map((message) => message.messageId));
183
229
  }
184
230
  }
185
231
  if (msg.type === "task" && msg.agentId && this.taskHandler) {
package/src/protocol.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type {
2
+ AgentChatProfile,
2
3
  AgentHeartbeat,
3
4
  AgentResultFile,
4
5
  AgentComplete,