openclaw-agentchat 0.2.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-agentchat",
3
- "version": "0.2.1",
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
  }