@newbase-clawchat/openclaw-clawchat 2026.5.12-3 → 2026.5.12-5

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.
Files changed (50) hide show
  1. package/README.md +13 -8
  2. package/dist/index.js +1 -1
  3. package/dist/src/channel.js +3 -0
  4. package/dist/src/channel.setup.js +1 -4
  5. package/dist/src/client.js +31 -93
  6. package/dist/src/inbound.js +1 -1
  7. package/dist/src/message-mapper.js +1 -1
  8. package/dist/src/mock-transport.js +31 -0
  9. package/dist/src/outbound.js +22 -46
  10. package/dist/src/protocol-types.js +49 -0
  11. package/dist/src/protocol-types.typecheck.js +1 -0
  12. package/dist/src/protocol.js +2 -2
  13. package/dist/src/reply-dispatcher.js +1 -1
  14. package/dist/src/runtime.js +4 -4
  15. package/dist/src/ws-alignment.js +1 -0
  16. package/dist/src/ws-client.js +561 -0
  17. package/index.ts +1 -1
  18. package/openclaw.plugin.json +2 -2
  19. package/package.json +2 -3
  20. package/src/buffered-stream.test.ts +1 -1
  21. package/src/buffered-stream.ts +1 -1
  22. package/src/channel.outbound.test.ts +92 -51
  23. package/src/channel.setup.ts +2 -5
  24. package/src/channel.test.ts +7 -0
  25. package/src/channel.ts +3 -0
  26. package/src/client.test.ts +56 -48
  27. package/src/client.ts +37 -129
  28. package/src/inbound.test.ts +10 -10
  29. package/src/inbound.ts +1 -1
  30. package/src/manifest.test.ts +4 -4
  31. package/src/media-runtime.ts +5 -8
  32. package/src/message-mapper.test.ts +2 -2
  33. package/src/message-mapper.ts +2 -2
  34. package/src/mock-transport.test.ts +35 -0
  35. package/src/mock-transport.ts +38 -0
  36. package/src/outbound.test.ts +185 -96
  37. package/src/outbound.ts +27 -57
  38. package/src/protocol-types.test.ts +69 -0
  39. package/src/protocol-types.ts +270 -0
  40. package/src/protocol-types.typecheck.ts +89 -0
  41. package/src/protocol.ts +2 -2
  42. package/src/reply-dispatcher.test.ts +90 -134
  43. package/src/reply-dispatcher.ts +3 -2
  44. package/src/runtime.test.ts +174 -18
  45. package/src/runtime.ts +5 -5
  46. package/src/streaming.test.ts +1 -1
  47. package/src/streaming.ts +1 -1
  48. package/src/ws-alignment.ts +2 -0
  49. package/src/ws-client.test.ts +1023 -0
  50. package/src/ws-client.ts +633 -0
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @newbase-clawchat/openclaw-clawchat
2
2
 
3
- OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Protocol v2, using [`@newbase-clawchat/sdk`](https://www.npmjs.com/package/@newbase-clawchat/sdk) for the WebSocket transport plus a small REST surface for profile / social / media operations (`/v1/*` plus unversioned `/media/upload`).
3
+ OpenClaw channel plugin that connects an agent to ClawChat over ClawChat Protocol v2 with a plugin-owned WebSocket client, plus a small REST surface for profile / social / media operations (`/v1/*` plus unversioned `/media/upload`).
4
4
 
5
5
  ## Features
6
6
 
7
- - WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
7
+ - Plugin-owned WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
8
8
  - Invite-code onboarding — no raw credentials required
9
9
  - Inbound `message.send` / `message.reply` with reply context
10
10
  - Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
@@ -67,10 +67,14 @@ omitting it from the `channels add` CLI catalog. If `channels add` fails with
67
67
  `Unknown channel: openclaw-clawchat`, use `/clawchat-login A1B2C3` after a real
68
68
  Gateway restart.
69
69
 
70
- After a successful activation on a running Gateway with hot config reload,
71
- OpenClaw should reload the plugin registry and start the channel without a hard
72
- restart. Restart the Gateway only after installing/updating the plugin, when
73
- config reload is disabled, or when the channel probe does not become healthy:
70
+ After a successful activation on a running Gateway with config reload, OpenClaw
71
+ should load the full runtime plugin and start the channel automatically. If the
72
+ Gateway only has the setup-only entry loaded, the credential write forces a
73
+ Gateway reload/restart instead of a setup-only channel hot restart; after the full
74
+ runtime is attached, later channel config changes can hot reload the channel.
75
+ Restart the Gateway manually only after installing/updating the plugin when the
76
+ automatic restart has not happened, when config reload is disabled, or when the
77
+ channel probe does not become healthy:
74
78
 
75
79
  ```bash
76
80
  openclaw gateway restart
@@ -97,7 +101,8 @@ exists, and ensures tool policy covers the plugin. If `tools.allow` or
97
101
  id to `tools.alsoAllow` so policy-restricted agents can execute the tools.
98
102
  Before activation, account/media tools return a config error instead of
99
103
  disappearing; after activation/login, the channel is enabled and the same tools
100
- read the persisted token/userId after hot config reload or a Gateway restart.
104
+ read the persisted token/userId after the runtime plugin reloads or the Gateway
105
+ restarts.
101
106
 
102
107
  After activation/login, the channel section is enabled and has credentials:
103
108
 
@@ -145,7 +150,7 @@ Then open the printed URL (default `http://127.0.0.1:4318`) to exercise the plug
145
150
  src/
146
151
  channel.ts plugin adapter (setup, auth.login, gateway, agentPrompt)
147
152
  runtime.ts inbound dispatch + reply dispatcher
148
- client.ts chat-sdk WebSocket client wrapper
153
+ client.ts ClawChat WebSocket client adapter and stream helpers
149
154
  api-client.ts REST client for /v1/* + /media/upload
150
155
  inbound.ts envelope → agent turn
151
156
  outbound.ts agent reply → envelope
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { registerOpenclawClawlingTools } from "./src/tools.js";
7
7
  export default defineChannelPluginEntry({
8
8
  id: "openclaw-clawchat",
9
9
  name: "Clawling Chat",
10
- description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
10
+ description: "Clawling Chat Protocol v2 channel plugin",
11
11
  plugin: openclawClawlingPlugin,
12
12
  configSchema: { schema: openclawClawlingConfigSchema },
13
13
  setRuntime: setOpenclawClawlingRuntime,
@@ -11,6 +11,9 @@ const CLAWCHAT_PLATFORM_PROMPT = "You are replying through ClawChat, a chat-firs
11
11
  export const openclawClawlingPlugin = createChatChannelPlugin({
12
12
  base: {
13
13
  ...openclawClawlingSetupPlugin,
14
+ reload: {
15
+ configPrefixes: [`channels.${CHANNEL_ID}`],
16
+ },
14
17
  directory: createEmptyChannelDirectoryAdapter(),
15
18
  auth: {
16
19
  login: async ({ cfg, accountId, runtime }) => {
@@ -84,7 +84,7 @@ export const openclawClawlingSetupPlugin = {
84
84
  selectionLabel: "Clawling Chat",
85
85
  docsPath: "/channels/openclaw-clawchat",
86
86
  docsLabel: "openclaw-clawchat",
87
- blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
87
+ blurb: "ClawChat Protocol v2 over WebSocket.",
88
88
  order: 110,
89
89
  },
90
90
  capabilities: {
@@ -95,9 +95,6 @@ export const openclawClawlingSetupPlugin = {
95
95
  polls: false,
96
96
  blockStreaming: true,
97
97
  },
98
- reload: {
99
- configPrefixes: [`channels.${CHANNEL_ID}`],
100
- },
101
98
  configSchema: {
102
99
  schema: openclawClawlingConfigSchema,
103
100
  },
@@ -1,74 +1,17 @@
1
- import { createWSClient, } from "@newbase-clawchat/sdk";
2
- function createMsghubConnectTransport(account, transport, lifecycle) {
3
- const maybeWrapped = transport;
4
- if (maybeWrapped.__openclawInnerTransport)
5
- return transport;
6
- const wrapped = {
7
- __openclawInnerTransport: transport,
8
- get state() {
9
- return transport.state;
10
- },
11
- connect(url, handlers) {
12
- return transport.connect(url, handlers);
13
- },
14
- send(data) {
15
- let parsed;
16
- try {
17
- parsed = JSON.parse(data);
18
- }
19
- catch {
20
- transport.send(data);
21
- return;
22
- }
23
- if (!parsed ||
24
- typeof parsed !== "object" ||
25
- parsed.event !== "connect") {
26
- transport.send(data);
27
- return;
28
- }
29
- const env = parsed;
30
- const nonce = typeof env.payload?.nonce === "string" ? env.payload.nonce : "";
31
- const payload = {
32
- token: account.token,
33
- nonce,
34
- ...(account.userId ? { device_id: account.userId } : {}),
35
- capabilities: {
36
- multi_device: true,
37
- device_replay: true,
38
- },
39
- };
40
- const connectEnv = { ...env, payload };
41
- transport.send(JSON.stringify(connectEnv));
42
- lifecycle?.onConnectFrameSent?.(connectEnv);
43
- },
44
- close(code, reason) {
45
- transport.close(code, reason);
46
- },
47
- };
48
- return wrapped;
49
- }
50
- function installMsghubConnectTransport(account, client, lifecycle) {
51
- const inner = client;
52
- if (!inner.opts?.transport)
53
- return;
54
- inner.opts.transport = createMsghubConnectTransport(account, inner.opts.transport, lifecycle);
55
- }
1
+ import { createClawChatClient } from "./ws-client.js";
56
2
  export function createOpenclawClawlingClient(account, overrides = {}) {
57
- // Only forward a finite `maxRetries` to the SDK — the SDK's own default
58
- // is already unbounded, so omitting the field keeps that behavior. This
59
- // avoids forcing the SDK to special-case `Infinity`.
60
- const maxRetries = account.reconnect.maxRetries;
61
- const reconnect = {
62
- enabled: true,
63
- initialDelay: account.reconnect.initialDelay,
64
- maxDelay: account.reconnect.maxDelay,
65
- jitterRatio: account.reconnect.jitterRatio,
66
- ...(Number.isFinite(maxRetries) ? { maxRetries } : {}),
67
- };
68
- const options = {
3
+ const client = createClawChatClient({
69
4
  url: account.websocketUrl,
70
5
  token: account.token,
71
- reconnect,
6
+ deviceId: account.userId,
7
+ ...(overrides.transport ? { transport: overrides.transport } : {}),
8
+ reconnect: {
9
+ enabled: true,
10
+ initialDelay: account.reconnect.initialDelay,
11
+ maxDelay: account.reconnect.maxDelay,
12
+ jitterRatio: account.reconnect.jitterRatio,
13
+ maxRetries: account.reconnect.maxRetries,
14
+ },
72
15
  heartbeat: {
73
16
  enabled: true,
74
17
  interval: account.heartbeat.interval,
@@ -78,14 +21,16 @@ export function createOpenclawClawlingClient(account, overrides = {}) {
78
21
  timeout: account.ack.timeout,
79
22
  autoResendOnTimeout: account.ack.autoResendOnTimeout,
80
23
  },
81
- // Buffer outbound sends during the tiny reconnect window so an inbound
82
- // message isn't silently dropped while the socket is flapping.
83
- queueWhileReconnecting: true,
84
- ...(overrides.transport ? { transport: overrides.transport } : {}),
85
- ...(overrides.logger ? { logger: overrides.logger } : {}),
86
- };
87
- const client = createWSClient(options);
88
- installMsghubConnectTransport(account, client, overrides.wsLifecycle);
24
+ });
25
+ if (overrides.wsLifecycle?.onConnectFrameSent) {
26
+ const sendRawEnvelope = client.sendRawEnvelope.bind(client);
27
+ client.sendRawEnvelope = (env) => {
28
+ sendRawEnvelope(env);
29
+ if (env.event === "connect") {
30
+ overrides.wsLifecycle?.onConnectFrameSent?.(env);
31
+ }
32
+ };
33
+ }
89
34
  return client;
90
35
  }
91
36
  function normalizeRouting(params) {
@@ -97,31 +42,26 @@ function normalizeRouting(params) {
97
42
  throw new Error("openclaw-clawchat streaming emit requires routing");
98
43
  }
99
44
  /**
100
- * Emit a raw v2 envelope directly over the transport so we can carry top-level
101
- * `chat_id` routing without SDK-injected `to` metadata.
45
+ * Emit a raw v2 envelope through the local client so stream helpers carry
46
+ * top-level `chat_id` routing without legacy `to` metadata.
102
47
  */
103
48
  function emitEnvelope(client, event, payload, routing, options = {}) {
104
- const inner = client;
105
- if (!options.forceRawTransport && inner.emitRaw) {
106
- inner.emitRaw(event, payload, { chat_id: routing.chatId });
49
+ if (!options.forceRawTransport) {
50
+ client.emitRaw(event, payload, { chat_id: routing.chatId });
107
51
  return;
108
52
  }
109
- if (!inner.opts?.transport) {
110
- if (!options.forceRawTransport && inner.emitRaw) {
111
- inner.emitRaw(event, payload, { chat_id: routing.chatId });
112
- return;
113
- }
114
- throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
53
+ if (typeof client.nextTraceId !== "function" || typeof client.sendRawEnvelope !== "function") {
54
+ throw new Error("openclaw-clawchat streaming emit requires local raw transport");
115
55
  }
116
56
  const env = {
117
57
  version: "2",
118
58
  event,
119
- trace_id: inner.opts.traceIdFactory(),
59
+ trace_id: client.nextTraceId(),
120
60
  emitted_at: Date.now(),
121
61
  chat_id: routing.chatId,
122
62
  payload,
123
63
  };
124
- inner.opts.transport.send(JSON.stringify(env));
64
+ client.sendRawEnvelope(env);
125
65
  }
126
66
  /**
127
67
  * Emit a minimal `message.created` envelope to open a streaming message.
@@ -189,10 +129,8 @@ export function emitStreamDone(client, params) {
189
129
  * the same `payload.message_id` as the preceding `message.created` /
190
130
  * `message.add` / `message.done` frames.
191
131
  *
192
- * The SDK's high-level `client.replyMessage()` disallows `payload.message_id`
193
- * on outbound replies (the server normally assigns one via ack); for the
194
- * streaming-finalize use case the backend expects the correlated id, so we
195
- * bypass the SDK validator and write directly to the transport.
132
+ * Final stream replies include the correlated `payload.message_id`, so they
133
+ * use the local raw-envelope API instead of any higher-level ackable send.
196
134
  */
197
135
  export function emitFinalStreamReply(client, params) {
198
136
  const routing = normalizeRouting(params);
@@ -1,4 +1,4 @@
1
- import { EVENT, } from "@newbase-clawchat/sdk";
1
+ import { EVENT, } from "./protocol-types.js";
2
2
  import { extractMediaFragments, fragmentsToText } from "./message-mapper.js";
3
3
  import { hasRenderableText, isInboundMessagePayload } from "./protocol.js";
4
4
  const DEDUP_MAX = 256;
@@ -53,7 +53,7 @@ export function textToFragments(text) {
53
53
  /**
54
54
  * Extract media fragments from a body (image/file/audio/video). Skips
55
55
  * entries missing `url`. Preserves all optional metadata fields the
56
- * SDK passes through (mime/size/width/height/duration/name).
56
+ * protocol carries through (mime/size/width/height/duration/name).
57
57
  */
58
58
  export function extractMediaFragments(fragments) {
59
59
  const out = [];
@@ -0,0 +1,31 @@
1
+ export class MockTransport {
2
+ handlers;
3
+ currentState = "closed";
4
+ sent = [];
5
+ get state() {
6
+ return this.currentState;
7
+ }
8
+ async connect(_url, handlers) {
9
+ this.handlers = handlers;
10
+ this.currentState = "open";
11
+ handlers.onOpen();
12
+ }
13
+ send(data) {
14
+ if (this.currentState !== "open") {
15
+ throw new Error("transport is not open");
16
+ }
17
+ this.sent.push(data);
18
+ }
19
+ close(code = 1000, reason = "client close") {
20
+ if (this.currentState === "closed")
21
+ return;
22
+ this.currentState = "closed";
23
+ this.handlers?.onClose(code, reason);
24
+ }
25
+ emitInbound(data) {
26
+ this.handlers?.onMessage(data);
27
+ }
28
+ emitError(err) {
29
+ this.handlers?.onError(err);
30
+ }
31
+ }
@@ -9,12 +9,6 @@ import { createAlignedWsQueue } from "./ws-alignment.js";
9
9
  import { formatWsLog } from "./ws-log.js";
10
10
  const alignedOutboundQueues = new WeakMap();
11
11
  const alignedOutboundContexts = new WeakMap();
12
- function getRawClientInternals(client) {
13
- const raw = client;
14
- if (!raw.opts?.transport || !raw.opts.traceIdFactory || !raw.on)
15
- return null;
16
- return raw;
17
- }
18
12
  function getAlignedOutboundQueue(client, account, log) {
19
13
  const existing = alignedOutboundQueues.get(client);
20
14
  if (existing)
@@ -36,21 +30,14 @@ export function setAlignedOutboundLogContext(client, context) {
36
30
  alignedOutboundContexts.set(client, context);
37
31
  }
38
32
  export function flushAlignedOutboundQueue(client) {
39
- const raw = getRawClientInternals(client);
40
- if (!raw?.opts?.transport)
41
- return;
42
33
  const queue = alignedOutboundQueues.get(client);
43
- queue?.flush((wire) => raw.opts.transport.send(wire));
34
+ queue?.flush((wire) => client.sendWire(wire));
44
35
  }
45
36
  export function getAlignedOutboundQueueSize(client) {
46
37
  return alignedOutboundQueues.get(client)?.snapshot().length ?? 0;
47
38
  }
48
39
  async function sendAlignedAckableEnvelope(params) {
49
- const raw = getRawClientInternals(params.client);
50
- if (!raw?.opts?.transport || !raw.opts.traceIdFactory)
51
- return null;
52
- const transport = raw.opts.transport;
53
- const traceId = raw.opts.traceIdFactory();
40
+ const traceId = params.client.nextTraceId();
54
41
  const env = {
55
42
  version: "2",
56
43
  event: params.eventName,
@@ -61,15 +48,16 @@ async function sendAlignedAckableEnvelope(params) {
61
48
  };
62
49
  const wire = JSON.stringify(env);
63
50
  const queue = getAlignedOutboundQueue(params.client, params.account, params.log);
51
+ const isReady = () => {
52
+ const state = params.client.state;
53
+ return params.client.transportState === "open" && (!state || state === "connected");
54
+ };
64
55
  return await new Promise((resolve, reject) => {
65
56
  let timer;
66
57
  const cleanup = () => {
67
58
  if (timer)
68
59
  clearTimeout(timer);
69
- if (raw.off)
70
- raw.off("raw", onRaw);
71
- else
72
- raw.removeListener?.("raw", onRaw);
60
+ params.client.off("raw", onRaw);
73
61
  };
74
62
  const logAck = (event, action, fields) => {
75
63
  params.log?.info?.(formatWsLog({
@@ -78,7 +66,7 @@ async function sendAlignedAckableEnvelope(params) {
78
66
  ...(alignedOutboundContexts.get(params.client)?.() ?? {
79
67
  attempt: 1,
80
68
  reconnectCount: 0,
81
- state: transport.state === "open" ? "ready" : "reconnecting",
69
+ state: isReady() ? "ready" : "reconnecting",
82
70
  }),
83
71
  action,
84
72
  fields: [
@@ -98,7 +86,7 @@ async function sendAlignedAckableEnvelope(params) {
98
86
  resolve(ack);
99
87
  };
100
88
  const startAckTimer = () => {
101
- raw.on?.("raw", onRaw);
89
+ params.client.on("raw", onRaw);
102
90
  timer = setTimeout(() => {
103
91
  cleanup();
104
92
  logAck("ack_timeout", "reject_no_reconnect", [["timeout_ms", params.account.ack.timeout]]);
@@ -111,18 +99,22 @@ async function sendAlignedAckableEnvelope(params) {
111
99
  chatId: params.chatId,
112
100
  wire,
113
101
  onWrite: startAckTimer,
102
+ onDrop: () => {
103
+ cleanup();
104
+ reject(new Error(`send queue full; dropped ${params.eventName} before write for trace_id=${traceId}`));
105
+ },
114
106
  };
115
- if (transport.state !== "open") {
107
+ if (!isReady()) {
116
108
  queue.enqueue(item);
117
109
  return;
118
110
  }
119
111
  try {
120
112
  queue.enqueue(item);
121
- queue.flush((queuedWire) => transport.send(queuedWire));
113
+ queue.flush((queuedWire) => params.client.sendWire(queuedWire));
122
114
  }
123
- catch (err) {
124
- cleanup();
125
- reject(err);
115
+ catch {
116
+ // The queue keeps the failed frame at the head for reconnect retry, so
117
+ // keep this promise pending until the frame is written+acked, dropped, or timed out.
126
118
  }
127
119
  });
128
120
  }
@@ -185,8 +177,8 @@ export async function sendOpenclawClawlingText(params) {
185
177
  }
186
178
  const mentions = params.mentions ?? [];
187
179
  const textFragments = text ? textToFragments(text) : [];
188
- // Cast at the SDK boundary: each MediaItem object is structurally compatible
189
- // with one of the SDK's narrow Fragment members (ImageFragment / FileFragment /
180
+ // Each MediaItem object is structurally compatible
181
+ // with one of the local narrow Fragment members (ImageFragment / FileFragment /
190
182
  // AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
191
183
  // shape lets us build a single uniform array without a per-kind switch.
192
184
  const fragments = [...textFragments, ...richFragments, ...mediaFragments];
@@ -212,24 +204,13 @@ export async function sendOpenclawClawlingText(params) {
212
204
  },
213
205
  },
214
206
  };
215
- ack = (await sendAlignedAckableEnvelope({
207
+ ack = await sendAlignedAckableEnvelope({
216
208
  client: params.client,
217
209
  account: params.account,
218
210
  eventName: "message.reply",
219
211
  chatId: params.to.chatId,
220
212
  payload,
221
213
  ...(params.log ? { log: params.log } : {}),
222
- })) ?? await params.client.replyMessage({
223
- chat_id: params.to.chatId,
224
- mode: "normal",
225
- replyTo: {
226
- msgId: params.replyCtx.replyToMessageId,
227
- senderId: params.replyCtx.replyPreviewSenderId,
228
- nickName: params.replyCtx.replyPreviewNickName,
229
- fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
230
- },
231
- body: { fragments },
232
- context: { mentions },
233
214
  });
234
215
  }
235
216
  else {
@@ -241,18 +222,13 @@ export async function sendOpenclawClawlingText(params) {
241
222
  context: { mentions, reply: null },
242
223
  },
243
224
  };
244
- ack = (await sendAlignedAckableEnvelope({
225
+ ack = await sendAlignedAckableEnvelope({
245
226
  client: params.client,
246
227
  account: params.account,
247
228
  eventName: "message.send",
248
229
  chatId: params.to.chatId,
249
230
  payload,
250
231
  ...(params.log ? { log: params.log } : {}),
251
- })) ?? await params.client.sendMessage({
252
- chat_id: params.to.chatId,
253
- mode: "normal",
254
- body: { fragments },
255
- context: { mentions, reply: null },
256
232
  });
257
233
  }
258
234
  params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`);
@@ -0,0 +1,49 @@
1
+ export const EVENT = {
2
+ CONNECT_CHALLENGE: "connect.challenge",
3
+ CONNECT: "connect",
4
+ HELLO_OK: "hello-ok",
5
+ HELLO_FAIL: "hello-fail",
6
+ MESSAGE_SEND: "message.send",
7
+ MESSAGE_ACK: "message.ack",
8
+ MESSAGE_REPLY: "message.reply",
9
+ MESSAGE_CREATED: "message.created",
10
+ MESSAGE_ADD: "message.add",
11
+ MESSAGE_DONE: "message.done",
12
+ MESSAGE_FAILED: "message.failed",
13
+ TYPING_UPDATE: "typing.update",
14
+ OFFLINE_BATCH: "offline.batch",
15
+ OFFLINE_ACK: "offline.ack",
16
+ OFFLINE_DONE: "offline.done",
17
+ PING: "ping",
18
+ PONG: "pong",
19
+ };
20
+ export class AuthError extends Error {
21
+ name = "AuthError";
22
+ }
23
+ export class TransportError extends Error {
24
+ name = "TransportError";
25
+ }
26
+ export class ProtocolError extends Error {
27
+ envelope;
28
+ name = "ProtocolError";
29
+ constructor(message, envelope) {
30
+ super(message);
31
+ this.envelope = envelope;
32
+ }
33
+ }
34
+ export class AckTimeoutError extends Error {
35
+ traceId;
36
+ timeoutMs;
37
+ name = "AckTimeoutError";
38
+ constructor(traceId, timeoutMs) {
39
+ super(`ack timeout after ${timeoutMs}ms for trace_id=${traceId}`);
40
+ this.traceId = traceId;
41
+ this.timeoutMs = timeoutMs;
42
+ }
43
+ }
44
+ export class StateError extends Error {
45
+ name = "StateError";
46
+ }
47
+ export function isBusinessDispatchEvent(event) {
48
+ return event === EVENT.MESSAGE_SEND || event === EVENT.MESSAGE_REPLY || event === EVENT.MESSAGE_DONE;
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Local narrow guards for inbound protocol envelopes.
3
3
  *
4
- * The SDK's `message` event hands us `Envelope<unknown>`. Before casting the
5
- * payload to `DownlinkMessageSendPayload` we run these cheap structural checks
4
+ * Raw client events hand us `Envelope<unknown>`. Before casting the
5
+ * payload to `MessagePayload` we run these cheap structural checks
6
6
  * so runtime errors surface as skipped messages, not crashes.
7
7
  */
8
8
  export function isInboundMessagePayload(payload) {
@@ -253,7 +253,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
253
253
  const bodyFragments = [
254
254
  ...(mergedText ? textToFragments(mergedText) : []),
255
255
  ...finalRichFragments,
256
- // mediaFragments is the local wide shape; cast at SDK boundary as
256
+ // mediaFragments is the local wide shape; cast to the local protocol fragment type as
257
257
  // we do in outbound.ts.
258
258
  ...mergedMedia,
259
259
  ];
@@ -1,4 +1,4 @@
1
- import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "@newbase-clawchat/sdk";
1
+ import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "./protocol-types.js";
2
2
  import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
3
3
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
4
4
  import { createOpenclawClawlingClient } from "./client.js";
@@ -354,7 +354,7 @@ export async function startOpenclawClawlingGateway(params) {
354
354
  log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
355
355
  }
356
356
  else {
357
- log?.error?.(`[${accountId}] openclaw-clawchat sdk error: ${classified.message}`);
357
+ log?.error?.(`[${accountId}] openclaw-clawchat client error: ${classified.message}`);
358
358
  }
359
359
  });
360
360
  client.on("message", async (env) => {
@@ -477,8 +477,8 @@ export async function startOpenclawClawlingGateway(params) {
477
477
  });
478
478
  // `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
479
479
  // (auth). Transport failures (server unreachable, DNS error, etc.) do
480
- // NOT reject this promise — the SDK catches them internally and drives
481
- // its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
480
+ // NOT reject this promise — the local client handles them internally and
481
+ // drives its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
482
482
  // capped at `maxDelay`, with jitter). So we never throw here on anything
483
483
  // other than auth failure; on auth we tear the account down cleanly and
484
484
  // return without throwing (which would make the gateway supervisor
@@ -34,6 +34,7 @@ export function createAlignedWsQueue(options) {
34
34
  ["queue_size", queue.length],
35
35
  ["queue_max", maxSize],
36
36
  ], context().state);
37
+ dropped.onDrop?.();
37
38
  }
38
39
  }
39
40
  queue.push(item);