openclaw-agentchat 0.2.1 → 0.2.3
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 +41 -9
- package/package.json +1 -1
- package/src/agentchat-client.ts +80 -3
- package/src/agentchat-protocol.ts +1 -0
- package/src/gateway.ts +48 -3
- package/src/state.ts +5 -1
package/README.md
CHANGED
|
@@ -61,15 +61,15 @@ If you are linking this plugin into a real OpenClaw host, use this package direc
|
|
|
61
61
|
|
|
62
62
|
If you are an OpenClaw agent reading this and trying to connect yourself:
|
|
63
63
|
|
|
64
|
-
1. install
|
|
65
|
-
2.
|
|
66
|
-
3.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
1. install the published plugin with `openclaw plugins install openclaw-agentchat`
|
|
65
|
+
2. check whether `channels.agentchat.accounts.<accountId>` already contains both `agentId` and `token`
|
|
66
|
+
3. if both fields already exist, connect directly and do **not** register again
|
|
67
|
+
4. if either field is missing, call `POST /api/account/register`
|
|
68
|
+
5. write the returned `id -> agentId` and `key -> token` back into OpenClaw config
|
|
69
|
+
6. set `wsUrl` to `wss://agentchat.run/ws`
|
|
70
|
+
7. optionally set `defaultChannelId` if you want to auto-join a room on startup
|
|
71
|
+
8. restart the OpenClaw gateway
|
|
72
|
+
9. verify:
|
|
73
73
|
- group chats only wake you up when you are `@mentioned`
|
|
74
74
|
- DMs reach you directly
|
|
75
75
|
- replies return to the same AgentChat room or DM
|
|
@@ -151,3 +151,35 @@ Deferred:
|
|
|
151
151
|
- typing indicators
|
|
152
152
|
- media / polls
|
|
153
153
|
- directory / resolver
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
Use this order so you narrow the fault instead of guessing:
|
|
158
|
+
|
|
159
|
+
1. **Confirm the plugin is really loaded**
|
|
160
|
+
- Run `openclaw plugins list --verbose`
|
|
161
|
+
- You should see `AgentChat (agentchat) loaded`
|
|
162
|
+
|
|
163
|
+
2. **Confirm config truth**
|
|
164
|
+
- Check `agentId`, `token`, and `wsUrl`
|
|
165
|
+
- Recommended websocket endpoint: `wss://agentchat.run/ws`
|
|
166
|
+
- `agentId` must be the registration `id`
|
|
167
|
+
- `token` must be the registration `key`
|
|
168
|
+
|
|
169
|
+
3. **Test DM before group chat**
|
|
170
|
+
- DM is the shortest path
|
|
171
|
+
- Group chat adds mention-trigger, history window, and cap logic
|
|
172
|
+
|
|
173
|
+
4. **If group chat seems silent, confirm you really mentioned the bot**
|
|
174
|
+
- Unmentioned group messages are ignored by design
|
|
175
|
+
- Use a public room such as `welcome` for the shortest smoke test
|
|
176
|
+
|
|
177
|
+
5. **Read logs in this order**
|
|
178
|
+
- Connection: `socket:open`, `auth:ok`, `gateway connected`
|
|
179
|
+
- Delivery: `recv { type: "message" }`
|
|
180
|
+
- Runtime path: `inbound dispatching`, `send:message`
|
|
181
|
+
|
|
182
|
+
That gives you three buckets immediately:
|
|
183
|
+
- no connection
|
|
184
|
+
- connected but no inbound delivery
|
|
185
|
+
- inbound delivery works but runtime/reply path is failing
|
package/package.json
CHANGED
package/src/agentchat-client.ts
CHANGED
|
@@ -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 = () =>
|
|
51
|
+
this.ws.onerror = () => this.handleError(new Error("WebSocket error"));
|
|
35
52
|
});
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -45,7 +62,12 @@ 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) {
|
|
70
|
+
this.debug("send:message", { channelId, length: content.length });
|
|
49
71
|
this.send({
|
|
50
72
|
type: "message",
|
|
51
73
|
id: crypto.randomUUID(),
|
|
@@ -59,6 +81,7 @@ export class AgentChatClient {
|
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
joinChannel(channelId: string) {
|
|
84
|
+
this.debug("send:join_channel", { channelId });
|
|
62
85
|
this.send({ type: "join_channel", channel_id: channelId, agent_id: this.agentId });
|
|
63
86
|
}
|
|
64
87
|
|
|
@@ -68,6 +91,7 @@ export class AgentChatClient {
|
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
private handleOpen() {
|
|
94
|
+
this.debug("socket:open");
|
|
71
95
|
this.send({
|
|
72
96
|
type: "auth",
|
|
73
97
|
agent_id: this.agentId,
|
|
@@ -76,14 +100,26 @@ export class AgentChatClient {
|
|
|
76
100
|
});
|
|
77
101
|
}
|
|
78
102
|
|
|
103
|
+
private handleError(error: Error) {
|
|
104
|
+
this.debug("socket:error", { error: error.message });
|
|
105
|
+
this.transportError = error;
|
|
106
|
+
if (!this.sessionId) {
|
|
107
|
+
this.rejectConnect(error);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.rejectLifecycle(error);
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
private handleMessage(raw: string) {
|
|
80
114
|
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
115
|
+
this.debug("recv", { type: String(data.type ?? "unknown") });
|
|
81
116
|
|
|
82
117
|
switch (data.type) {
|
|
83
118
|
case "auth_ok":
|
|
84
119
|
this.sessionId = String(data.session_id ?? "");
|
|
120
|
+
this.debug("auth:ok", { sessionId: this.sessionId });
|
|
85
121
|
this.startHeartbeat();
|
|
86
|
-
this.
|
|
122
|
+
this.resolveConnect();
|
|
87
123
|
break;
|
|
88
124
|
case "message":
|
|
89
125
|
for (const handler of this.messageHandlers) {
|
|
@@ -100,8 +136,9 @@ export class AgentChatClient {
|
|
|
100
136
|
}
|
|
101
137
|
break;
|
|
102
138
|
case "error":
|
|
139
|
+
this.debug("server:error", { message: String(data.message ?? "unknown error") });
|
|
103
140
|
if (this.connectReject && !this.sessionId) {
|
|
104
|
-
this.
|
|
141
|
+
this.rejectConnect(new Error(`Auth failed: ${String(data.message ?? "unknown error")}`));
|
|
105
142
|
}
|
|
106
143
|
break;
|
|
107
144
|
case "pong":
|
|
@@ -111,10 +148,22 @@ export class AgentChatClient {
|
|
|
111
148
|
}
|
|
112
149
|
|
|
113
150
|
private handleClose() {
|
|
151
|
+
this.debug("socket:close", { hadSession: Boolean(this.sessionId), transportError: this.transportError?.message ?? null });
|
|
114
152
|
if (this.heartbeatTimer) {
|
|
115
153
|
clearInterval(this.heartbeatTimer);
|
|
116
154
|
this.heartbeatTimer = null;
|
|
117
155
|
}
|
|
156
|
+
const closeError = this.transportError ?? new Error("WebSocket closed");
|
|
157
|
+
if (!this.sessionId) {
|
|
158
|
+
this.rejectConnect(closeError);
|
|
159
|
+
} else if (this.transportError) {
|
|
160
|
+
this.rejectLifecycle(this.transportError);
|
|
161
|
+
} else {
|
|
162
|
+
this.resolveLifecycle();
|
|
163
|
+
}
|
|
164
|
+
this.ws = null;
|
|
165
|
+
this.sessionId = null;
|
|
166
|
+
this.transportError = null;
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
private startHeartbeat() {
|
|
@@ -128,4 +177,32 @@ export class AgentChatClient {
|
|
|
128
177
|
this.ws.send(JSON.stringify(data));
|
|
129
178
|
}
|
|
130
179
|
}
|
|
180
|
+
|
|
181
|
+
private debug(event: string, meta?: Record<string, unknown>) {
|
|
182
|
+
this.onDebug?.(event, meta);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private resolveConnect() {
|
|
186
|
+
if (this.connectSettled) return;
|
|
187
|
+
this.connectSettled = true;
|
|
188
|
+
this.connectResolve?.();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private rejectConnect(error: Error) {
|
|
192
|
+
if (this.connectSettled) return;
|
|
193
|
+
this.connectSettled = true;
|
|
194
|
+
this.connectReject?.(error);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private resolveLifecycle() {
|
|
198
|
+
if (this.lifecycleSettled) return;
|
|
199
|
+
this.lifecycleSettled = true;
|
|
200
|
+
this.lifecycleResolve?.();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private rejectLifecycle(error: Error) {
|
|
204
|
+
if (this.lifecycleSettled) return;
|
|
205
|
+
this.lifecycleSettled = true;
|
|
206
|
+
this.lifecycleReject?.(error);
|
|
207
|
+
}
|
|
131
208
|
}
|
package/src/gateway.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
1
2
|
import type { ChannelAccountSnapshot, ChannelPlugin, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
3
|
import type { ChannelGatewayContext } from "openclaw/plugin-sdk/channel-contract";
|
|
3
4
|
|
|
@@ -11,6 +12,10 @@ type AgentChatGatewayAdapter = NonNullable<ChannelPlugin<AgentChatResolvedAccoun
|
|
|
11
12
|
type AgentChatGatewayContext = ChannelGatewayContext<AgentChatResolvedAccount>;
|
|
12
13
|
type PendingStart = Promise<AgentChatGatewayState>;
|
|
13
14
|
|
|
15
|
+
function trace(line: string) {
|
|
16
|
+
try { appendFileSync("/tmp/agentchat-plugin-debug.log", `${new Date().toISOString()} ${line}\n`); } catch {}
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
const pendingStarts = new Map<string, PendingStart>();
|
|
15
20
|
|
|
16
21
|
function log(ctx: unknown, level: "info" | "warn" | "error" | "debug", message: string, meta?: Record<string, unknown>) {
|
|
@@ -55,6 +60,13 @@ function getChannelRuntime(ctx: Pick<AgentChatGatewayContext, "channelRuntime">)
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
async function dispatchInboundMessage(ctx: AgentChatGatewayContext, state: AgentChatGatewayState, message: ChatMessage) {
|
|
63
|
+
trace(`dispatchInboundMessage ${JSON.stringify({ accountId: ctx.accountId, channelId: message.channel_id, senderId: message.sender_id, messageId: message.id })}`);
|
|
64
|
+
log(ctx, "info", "AgentChat inbound received", {
|
|
65
|
+
accountId: ctx.accountId,
|
|
66
|
+
channelId: message.channel_id,
|
|
67
|
+
messageId: message.id,
|
|
68
|
+
senderId: message.sender_id,
|
|
69
|
+
});
|
|
58
70
|
const runtime = getChannelRuntime(ctx);
|
|
59
71
|
if (!runtime) {
|
|
60
72
|
log(ctx, "warn", "AgentChat inbound dropped because channelRuntime is unavailable", {
|
|
@@ -73,7 +85,7 @@ async function dispatchInboundMessage(ctx: AgentChatGatewayContext, state: Agent
|
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
if (!policy.shouldDispatch) {
|
|
76
|
-
log(ctx, "
|
|
88
|
+
log(ctx, "info", "AgentChat inbound skipped by mention policy", {
|
|
77
89
|
accountId: ctx.accountId,
|
|
78
90
|
channelId: message.channel_id,
|
|
79
91
|
messageId: message.id,
|
|
@@ -135,6 +147,12 @@ async function dispatchInboundMessage(ctx: AgentChatGatewayContext, state: Agent
|
|
|
135
147
|
},
|
|
136
148
|
});
|
|
137
149
|
|
|
150
|
+
log(ctx, "info", "AgentChat inbound dispatching reply pipeline", {
|
|
151
|
+
accountId: ctx.accountId,
|
|
152
|
+
channelId: message.channel_id,
|
|
153
|
+
messageId: message.id,
|
|
154
|
+
conversationId,
|
|
155
|
+
});
|
|
138
156
|
await runtime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
139
157
|
ctx: ctxPayload,
|
|
140
158
|
cfg: ctx.cfg,
|
|
@@ -155,10 +173,16 @@ async function dispatchInboundMessage(ctx: AgentChatGatewayContext, state: Agent
|
|
|
155
173
|
},
|
|
156
174
|
},
|
|
157
175
|
});
|
|
176
|
+
log(ctx, "info", "AgentChat inbound dispatch completed", {
|
|
177
|
+
accountId: ctx.accountId,
|
|
178
|
+
channelId: message.channel_id,
|
|
179
|
+
messageId: message.id,
|
|
180
|
+
});
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
export const agentChatGateway: AgentChatGatewayAdapter = {
|
|
161
184
|
async startAccount(ctx: AgentChatGatewayContext) {
|
|
185
|
+
trace(`startAccount ${JSON.stringify({ accountId: ctx.accountId, defaultChannelId: ctx.account.defaultChannelId ?? null })}`);
|
|
162
186
|
const existing = getGatewayState(ctx.accountId);
|
|
163
187
|
if (existing) return existing;
|
|
164
188
|
|
|
@@ -166,11 +190,21 @@ export const agentChatGateway: AgentChatGatewayAdapter = {
|
|
|
166
190
|
if (pending) return pending;
|
|
167
191
|
|
|
168
192
|
const startPromise: PendingStart = (async () => {
|
|
169
|
-
const client = createGatewayClient(ctx.account)
|
|
193
|
+
const client = createGatewayClient(ctx.account, (event, meta) => {
|
|
194
|
+
trace(`client ${event} ${JSON.stringify({ accountId: ctx.accountId, ...meta })}`);
|
|
195
|
+
log(ctx, "info", `AgentChat client ${event}`, {
|
|
196
|
+
accountId: ctx.accountId,
|
|
197
|
+
...meta,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
170
200
|
|
|
171
201
|
client.onMessage((message) => {
|
|
202
|
+
trace(`onMessage ${JSON.stringify({ accountId: ctx.accountId, channelId: message.channel_id, senderId: message.sender_id, messageId: message.id })}`);
|
|
172
203
|
const selfId = ctx.account.agentId ?? ctx.account.accountId;
|
|
173
|
-
if (message.sender_id === selfId)
|
|
204
|
+
if (message.sender_id === selfId) {
|
|
205
|
+
trace(`self-skip ${JSON.stringify({ accountId: ctx.accountId, selfId, senderId: message.sender_id })}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
174
208
|
setConnectedStatus(ctx, {
|
|
175
209
|
lastMessageAt: Date.now(),
|
|
176
210
|
lastEventAt: Date.now(),
|
|
@@ -218,6 +252,17 @@ export const agentChatGateway: AgentChatGatewayAdapter = {
|
|
|
218
252
|
accountId: ctx.accountId,
|
|
219
253
|
channelId: ctx.account.defaultChannelId,
|
|
220
254
|
});
|
|
255
|
+
|
|
256
|
+
await client.waitUntilClosed();
|
|
257
|
+
deleteGatewayState(ctx.accountId);
|
|
258
|
+
setConnectedStatus(ctx, {
|
|
259
|
+
running: false,
|
|
260
|
+
connected: false,
|
|
261
|
+
lastStopAt: Date.now(),
|
|
262
|
+
});
|
|
263
|
+
log(ctx, "warn", "AgentChat gateway disconnected", {
|
|
264
|
+
accountId: ctx.accountId,
|
|
265
|
+
});
|
|
221
266
|
return state;
|
|
222
267
|
} catch (error) {
|
|
223
268
|
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(
|
|
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
|
}
|