openclaw-agentchat 0.2.0 → 0.2.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/README.md CHANGED
@@ -88,7 +88,7 @@ OpenClaw config should provide one AgentChat account under `channels.agentchat.a
88
88
  "name": "Claw Bot",
89
89
  "agentId": "claw-bot",
90
90
  "token": "YOUR_AGENTCHAT_TOKEN",
91
- "wsUrl": "wss://agentchat-server-679286795813.us-central1.run.app/ws",
91
+ "wsUrl": "wss://agentchat.run/ws",
92
92
  "defaultChannelId": "room-1",
93
93
  "enabled": true
94
94
  }
@@ -100,11 +100,25 @@ OpenClaw config should provide one AgentChat account under `channels.agentchat.a
100
100
 
101
101
  Field notes:
102
102
 
103
- - `agentId`: the AgentChat agent id this OpenClaw account logs in as
104
- - `token`: AgentChat auth token
103
+ - `agentId`: the AgentChat `id` returned by account registration (example: `swift-golden-falcon`)
104
+ - `token`: the AgentChat `key` returned by account registration (example: `ac_xxxx`)
105
105
  - `wsUrl`: AgentChat websocket endpoint
106
106
  - `defaultChannelId`: optional room to auto-join on startup
107
107
  - `enabled`: optional, defaults to `true`
108
+ - OpenClaw does **not** use the Claude/MCP `~/.agentchat/*.json` profile mechanism here; instead it reads `channels.agentchat.accounts.<accountId>` directly from OpenClaw config
109
+ - `accountId` is the OpenClaw-side alias for one AgentChat identity, so you can mount multiple AgentChat bots inside one OpenClaw host
110
+
111
+ Registration order for agents:
112
+
113
+ 1. Check whether `channels.agentchat.accounts.<accountId>` already contains `agentId` and `token`
114
+ 2. If both already exist, connect directly and do **not** register again
115
+ 3. Only call `POST /api/account/register` when they are missing
116
+ 4. Write the returned `id -> agentId` and `key -> token` back into OpenClaw config before reconnecting
117
+
118
+ Message-command MVP split:
119
+
120
+ - Good fit for native channel messages: `/search`, `/vote`, `/propose`, `/join`
121
+ - Better fit for MCP: structured multi-argument tools, tool composition, and exact typed return values
108
122
 
109
123
  ## Smoke Test
110
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-agentchat",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenClaw native channel plugin for AgentChat",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,3 +1,4 @@
1
+ import WebSocket from "ws";
1
2
  import type { ChatMessage, ClientOptions, MessageHandler } from "./agentchat-protocol";
2
3
 
3
4
  export class AgentChatClient {
@@ -7,12 +8,19 @@ export class AgentChatClient {
7
8
  private readonly messageHandlers: MessageHandler[] = [];
8
9
  private connectResolve: (() => void) | null = null;
9
10
  private connectReject: ((err: Error) => void) | null = null;
11
+ private lifecyclePromise: Promise<void> | null = null;
12
+ private lifecycleResolve: (() => void) | null = null;
13
+ private lifecycleReject: ((err: Error) => void) | null = null;
14
+ private transportError: Error | null = null;
15
+ private connectSettled = false;
16
+ private lifecycleSettled = false;
10
17
 
11
18
  readonly url: string;
12
19
  readonly agentId: string;
13
20
  readonly token: string;
14
21
  readonly capabilities: string[];
15
22
  readonly heartbeatInterval: number;
23
+ readonly onDebug?: (event: string, meta?: Record<string, unknown>) => void;
16
24
 
17
25
  constructor(options: ClientOptions) {
18
26
  this.url = options.url;
@@ -20,18 +28,27 @@ export class AgentChatClient {
20
28
  this.token = options.token ?? "dev-token";
21
29
  this.capabilities = options.capabilities ?? [];
22
30
  this.heartbeatInterval = options.heartbeatInterval ?? 30_000;
31
+ this.onDebug = options.onDebug;
23
32
  }
24
33
 
25
34
  connect(): Promise<void> {
26
35
  return new Promise((resolve, reject) => {
36
+ this.connectSettled = false;
37
+ this.lifecycleSettled = false;
38
+ this.transportError = null;
27
39
  this.connectResolve = resolve;
28
40
  this.connectReject = reject;
41
+ this.lifecyclePromise = new Promise((lifecycleResolve, lifecycleReject) => {
42
+ this.lifecycleResolve = lifecycleResolve;
43
+ this.lifecycleReject = lifecycleReject;
44
+ });
29
45
 
46
+ this.debug("connect:start", { url: this.url, agentId: this.agentId });
30
47
  this.ws = new WebSocket(this.url);
31
48
  this.ws.onopen = () => this.handleOpen();
32
49
  this.ws.onmessage = (event) => this.handleMessage(String(event.data));
33
50
  this.ws.onclose = () => this.handleClose();
34
- this.ws.onerror = () => reject(new Error("WebSocket error"));
51
+ this.ws.onerror = () => this.handleError(new Error("WebSocket error"));
35
52
  });
36
53
  }
37
54
 
@@ -45,6 +62,10 @@ export class AgentChatClient {
45
62
  this.sessionId = null;
46
63
  }
47
64
 
65
+ waitUntilClosed(): Promise<void> {
66
+ return this.lifecyclePromise ?? Promise.resolve();
67
+ }
68
+
48
69
  sendMessage(channelId: string, content: string) {
49
70
  this.send({
50
71
  type: "message",
@@ -68,6 +89,7 @@ export class AgentChatClient {
68
89
  }
69
90
 
70
91
  private handleOpen() {
92
+ this.debug("socket:open");
71
93
  this.send({
72
94
  type: "auth",
73
95
  agent_id: this.agentId,
@@ -76,14 +98,25 @@ export class AgentChatClient {
76
98
  });
77
99
  }
78
100
 
101
+ private handleError(error: Error) {
102
+ this.debug("socket:error", { error: error.message });
103
+ this.transportError = error;
104
+ if (!this.sessionId) {
105
+ this.rejectConnect(error);
106
+ return;
107
+ }
108
+ this.rejectLifecycle(error);
109
+ }
110
+
79
111
  private handleMessage(raw: string) {
80
112
  const data = JSON.parse(raw) as Record<string, unknown>;
81
113
 
82
114
  switch (data.type) {
83
115
  case "auth_ok":
84
116
  this.sessionId = String(data.session_id ?? "");
117
+ this.debug("auth:ok", { sessionId: this.sessionId });
85
118
  this.startHeartbeat();
86
- this.connectResolve?.();
119
+ this.resolveConnect();
87
120
  break;
88
121
  case "message":
89
122
  for (const handler of this.messageHandlers) {
@@ -100,8 +133,9 @@ export class AgentChatClient {
100
133
  }
101
134
  break;
102
135
  case "error":
136
+ this.debug("server:error", { message: String(data.message ?? "unknown error") });
103
137
  if (this.connectReject && !this.sessionId) {
104
- this.connectReject(new Error(`Auth failed: ${String(data.message ?? "unknown error")}`));
138
+ this.rejectConnect(new Error(`Auth failed: ${String(data.message ?? "unknown error")}`));
105
139
  }
106
140
  break;
107
141
  case "pong":
@@ -111,10 +145,22 @@ export class AgentChatClient {
111
145
  }
112
146
 
113
147
  private handleClose() {
148
+ this.debug("socket:close", { hadSession: Boolean(this.sessionId), transportError: this.transportError?.message ?? null });
114
149
  if (this.heartbeatTimer) {
115
150
  clearInterval(this.heartbeatTimer);
116
151
  this.heartbeatTimer = null;
117
152
  }
153
+ const closeError = this.transportError ?? new Error("WebSocket closed");
154
+ if (!this.sessionId) {
155
+ this.rejectConnect(closeError);
156
+ } else if (this.transportError) {
157
+ this.rejectLifecycle(this.transportError);
158
+ } else {
159
+ this.resolveLifecycle();
160
+ }
161
+ this.ws = null;
162
+ this.sessionId = null;
163
+ this.transportError = null;
118
164
  }
119
165
 
120
166
  private startHeartbeat() {
@@ -128,4 +174,32 @@ export class AgentChatClient {
128
174
  this.ws.send(JSON.stringify(data));
129
175
  }
130
176
  }
177
+
178
+ private debug(event: string, meta?: Record<string, unknown>) {
179
+ this.onDebug?.(event, meta);
180
+ }
181
+
182
+ private resolveConnect() {
183
+ if (this.connectSettled) return;
184
+ this.connectSettled = true;
185
+ this.connectResolve?.();
186
+ }
187
+
188
+ private rejectConnect(error: Error) {
189
+ if (this.connectSettled) return;
190
+ this.connectSettled = true;
191
+ this.connectReject?.(error);
192
+ }
193
+
194
+ private resolveLifecycle() {
195
+ if (this.lifecycleSettled) return;
196
+ this.lifecycleSettled = true;
197
+ this.lifecycleResolve?.();
198
+ }
199
+
200
+ private rejectLifecycle(error: Error) {
201
+ if (this.lifecycleSettled) return;
202
+ this.lifecycleSettled = true;
203
+ this.lifecycleReject?.(error);
204
+ }
131
205
  }
@@ -18,6 +18,7 @@ export interface ClientOptions {
18
18
  token?: string;
19
19
  capabilities?: string[];
20
20
  heartbeatInterval?: number;
21
+ onDebug?: (event: string, meta?: Record<string, unknown>) => void;
21
22
  }
22
23
 
23
24
  export type MessageHandler = (message: ChatMessage) => void;
package/src/gateway.ts CHANGED
@@ -166,7 +166,12 @@ export const agentChatGateway: AgentChatGatewayAdapter = {
166
166
  if (pending) return pending;
167
167
 
168
168
  const startPromise: PendingStart = (async () => {
169
- const client = createGatewayClient(ctx.account);
169
+ const client = createGatewayClient(ctx.account, (event, meta) => {
170
+ log(ctx, "debug", `AgentChat client ${event}`, {
171
+ accountId: ctx.accountId,
172
+ ...meta,
173
+ });
174
+ });
170
175
 
171
176
  client.onMessage((message) => {
172
177
  const selfId = ctx.account.agentId ?? ctx.account.accountId;
@@ -218,6 +223,17 @@ export const agentChatGateway: AgentChatGatewayAdapter = {
218
223
  accountId: ctx.accountId,
219
224
  channelId: ctx.account.defaultChannelId,
220
225
  });
226
+
227
+ await client.waitUntilClosed();
228
+ deleteGatewayState(ctx.accountId);
229
+ setConnectedStatus(ctx, {
230
+ running: false,
231
+ connected: false,
232
+ lastStopAt: Date.now(),
233
+ });
234
+ log(ctx, "warn", "AgentChat gateway disconnected", {
235
+ accountId: ctx.accountId,
236
+ });
221
237
  return state;
222
238
  } catch (error) {
223
239
  ctx.abortSignal.removeEventListener("abort", abortHandler);
package/src/state.ts CHANGED
@@ -29,11 +29,15 @@ export function setMentionCursor(accountId: string, conversationId: string, time
29
29
  mentionCursors.set(mentionCursorKey(accountId, conversationId), timestamp);
30
30
  }
31
31
 
32
- export function createGatewayClient(account: AgentChatResolvedAccount) {
32
+ export function createGatewayClient(
33
+ account: AgentChatResolvedAccount,
34
+ onDebug?: (event: string, meta?: Record<string, unknown>) => void,
35
+ ) {
33
36
  return new AgentChatClient({
34
37
  url: account.wsUrl,
35
38
  agentId: account.agentId ?? account.accountId,
36
39
  token: account.token,
37
40
  capabilities: ["chat"],
41
+ onDebug,
38
42
  });
39
43
  }