openclaw-pincer 0.3.0 → 0.3.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.
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
3
+ * Adapted for Pincer protocol: REGISTER → AUTH → receive envelopes
3
4
  */
4
5
  export interface PincerConfig {
5
6
  baseUrl: string;
6
7
  token: string;
8
+ agentId: string;
9
+ agentName: string;
7
10
  }
8
11
  export declare const pincerChannel: {
9
12
  id: string;
@@ -1,19 +1,31 @@
1
1
  /**
2
2
  * channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
3
+ * Adapted for Pincer protocol: REGISTER → AUTH → receive envelopes
3
4
  */
4
5
  import WebSocket from "ws";
6
+ import { randomUUID } from "crypto";
5
7
  function resolveConfig(cfg) {
6
8
  return (cfg?.channels?.["openclaw-pincer"] ?? {});
7
9
  }
8
10
  function wsUrl(config) {
9
11
  const base = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
10
- return `${base}/api/v1/ws?token=${config.token}`;
12
+ return `${base}/ws?agent_id=${config.agentId}`;
13
+ }
14
+ function makeEnvelope(type, from, to, payload) {
15
+ return JSON.stringify({
16
+ id: randomUUID(),
17
+ type,
18
+ from,
19
+ to,
20
+ ts: new Date().toISOString(),
21
+ payload,
22
+ });
11
23
  }
12
24
  async function httpPost(config, path, body) {
13
25
  const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
14
26
  const res = await fetch(url, {
15
27
  method: "POST",
16
- headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
28
+ headers: { "X-API-Key": config.token, "Content-Type": "application/json" },
17
29
  body: JSON.stringify(body),
18
30
  });
19
31
  if (!res.ok)
@@ -22,13 +34,34 @@ async function httpPost(config, path, body) {
22
34
  function connectWs(params) {
23
35
  const { config, ctx, signal } = params;
24
36
  let retryMs = 1000;
37
+ let heartbeatTimer = null;
25
38
  function connect() {
26
39
  if (signal.aborted)
27
40
  return;
28
41
  const ws = new WebSocket(wsUrl(config));
29
42
  ws.on("open", () => {
30
43
  retryMs = 1000;
31
- console.log("[openclaw-pincer] WebSocket connected");
44
+ console.log("[openclaw-pincer] WebSocket connected, sending REGISTER + AUTH");
45
+ // Pincer handshake: REGISTER then AUTH
46
+ ws.send(makeEnvelope("REGISTER", config.agentId, "hub", {
47
+ name: config.agentName,
48
+ capabilities: [],
49
+ runtime_version: "openclaw/openclaw-pincer/0.3",
50
+ messaging_mode: "ws",
51
+ }));
52
+ ws.send(makeEnvelope("AUTH", config.agentId, "hub", {
53
+ api_key: config.token,
54
+ }));
55
+ // Heartbeat every 30s
56
+ if (heartbeatTimer)
57
+ clearInterval(heartbeatTimer);
58
+ heartbeatTimer = setInterval(() => {
59
+ if (ws.readyState === WebSocket.OPEN) {
60
+ ws.send(makeEnvelope("HEARTBEAT", config.agentId, "hub", {
61
+ agent_id: config.agentId,
62
+ }));
63
+ }
64
+ }, 30000);
32
65
  });
33
66
  ws.on("message", (data) => {
34
67
  try {
@@ -38,6 +71,10 @@ function connectWs(params) {
38
71
  catch { }
39
72
  });
40
73
  ws.on("close", () => {
74
+ if (heartbeatTimer) {
75
+ clearInterval(heartbeatTimer);
76
+ heartbeatTimer = null;
77
+ }
41
78
  if (signal.aborted)
42
79
  return;
43
80
  console.log(`[openclaw-pincer] WS closed, reconnecting in ${retryMs}ms`);
@@ -49,7 +86,13 @@ function connectWs(params) {
49
86
  console.error("[openclaw-pincer] WS error:", err.message);
50
87
  ws.close();
51
88
  });
52
- signal.addEventListener("abort", () => ws.close(), { once: true });
89
+ signal.addEventListener("abort", () => {
90
+ if (heartbeatTimer) {
91
+ clearInterval(heartbeatTimer);
92
+ heartbeatTimer = null;
93
+ }
94
+ ws.close();
95
+ }, { once: true });
53
96
  }
54
97
  connect();
55
98
  }
@@ -57,15 +100,95 @@ function handleMessage(config, ctx, msg) {
57
100
  const runtime = ctx.channelRuntime;
58
101
  if (!runtime)
59
102
  return;
60
- const sessionKey = msg.room_id
61
- ? `openclaw-pincer:channel:${msg.room_id}`
62
- : `openclaw-pincer:dm:${msg.from_agent_id ?? msg.sender_agent_id ?? "unknown"}`;
63
- const senderId = msg.from_agent_id ?? msg.sender_agent_id ?? "unknown";
64
- const text = msg.content ?? msg.payload?.text ?? "";
65
- if (!text)
103
+ const msgType = msg.type ?? "";
104
+ const payload = msg.payload ?? {};
105
+ const fromId = msg.from ?? "unknown";
106
+ // ACK just log
107
+ if (msgType === "ACK") {
108
+ if (payload.status === "ok") {
109
+ console.log("[openclaw-pincer] ✓ Authenticated with Pincer hub");
110
+ }
111
+ else {
112
+ console.error("[openclaw-pincer] AUTH failed:", payload.error);
113
+ }
114
+ return;
115
+ }
116
+ // HEARTBEAT_ACK — check for inbox
117
+ if (msgType === "HEARTBEAT_ACK" || msgType === "heartbeat.ack") {
118
+ const inbox = payload.inbox ?? [];
119
+ for (const item of inbox) {
120
+ const inner = item.payload ?? {};
121
+ const text = inner.text ?? JSON.stringify(inner);
122
+ const sender = item.from ?? "unknown";
123
+ dispatchToAgent(config, ctx, runtime, sender, text, undefined);
124
+ }
125
+ return;
126
+ }
127
+ // Ignore messages from self to prevent echo loops
128
+ if (fromId === config.agentId) {
66
129
  return;
67
- const peer = msg.room_id
68
- ? { kind: "group", id: msg.room_id }
130
+ }
131
+ // PING PONG
132
+ if (msgType === "PING") {
133
+ // We don't have ws ref here, but heartbeat keeps connection alive
134
+ return;
135
+ }
136
+ // DM message
137
+ if (msgType === "MESSAGE" || msgType === "agent.message") {
138
+ const text = payload.text ?? "";
139
+ if (!text)
140
+ return;
141
+ console.log(`[openclaw-pincer] 💬 DM from ${fromId.slice(0, 8)}: ${text.slice(0, 60)}`);
142
+ dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
143
+ return;
144
+ }
145
+ // Inbox delivery (reconnect catch-up)
146
+ if (msgType === "inbox.delivery") {
147
+ const items = Array.isArray(payload) ? payload : [payload];
148
+ for (const item of items) {
149
+ const inner = item.payload ?? {};
150
+ const text = inner.text ?? JSON.stringify(inner);
151
+ const sender = item.from ?? "unknown";
152
+ console.log(`[openclaw-pincer] 📬 Inbox from ${sender.slice(0, 8)}`);
153
+ dispatchToAgent(config, ctx, runtime, sender, text, undefined);
154
+ }
155
+ return;
156
+ }
157
+ // Room message
158
+ if (msgType === "room.message") {
159
+ const data = msg.data ?? msg.payload ?? {};
160
+ const sender = data.sender_agent_id ?? "unknown";
161
+ const content = data.content ?? "";
162
+ const roomId = data.room_id ?? msg.room_id ?? "";
163
+ if (sender === config.agentId || !content)
164
+ return;
165
+ console.log(`[openclaw-pincer] 💬 Room msg from ${sender.slice(0, 8)}: ${content.slice(0, 60)}`);
166
+ dispatchToAgent(config, ctx, runtime, sender, content, roomId);
167
+ return;
168
+ }
169
+ // TASK_ASSIGN
170
+ if (msgType === "TASK_ASSIGN" || msgType === "task.assigned") {
171
+ const taskId = payload.task_id ?? "?";
172
+ const title = payload.title ?? "";
173
+ const description = payload.description ?? "";
174
+ const text = `[Pincer Task]\ntask_id: ${taskId}\ntitle: ${title}\ndescription:\n${description}`;
175
+ console.log(`[openclaw-pincer] 📋 Task assigned: ${taskId.slice(0, 8)} ${title}`);
176
+ dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
177
+ return;
178
+ }
179
+ // BROADCAST
180
+ if (msgType === "BROADCAST" || msgType === "broadcast") {
181
+ const text = payload.text ?? JSON.stringify(payload);
182
+ console.log(`[openclaw-pincer] 📢 Broadcast: ${text.slice(0, 60)}`);
183
+ return;
184
+ }
185
+ }
186
+ function dispatchToAgent(config, ctx, runtime, senderId, text, roomId) {
187
+ const sessionKey = roomId
188
+ ? `openclaw-pincer:channel:${roomId}`
189
+ : `openclaw-pincer:dm:${senderId}`;
190
+ const peer = roomId
191
+ ? { kind: "group", id: roomId }
69
192
  : { kind: "direct", id: senderId };
70
193
  const route = runtime.routing.resolveAgentRoute({
71
194
  cfg: ctx.cfg,
@@ -85,14 +208,26 @@ function handleMessage(config, ctx, msg) {
85
208
  cfg: ctx.cfg,
86
209
  dispatcherOptions: {
87
210
  deliver: async (payload) => {
88
- if (msg.room_id) {
89
- await httpPost(config, `/rooms/${msg.room_id}/messages`, { content: payload.text });
211
+ try {
212
+ const text = payload.text ?? payload.content ?? JSON.stringify(payload);
213
+ console.log(`[openclaw-pincer] 📤 Delivering reply to ${senderId.slice(0, 8)}: ${text.slice(0, 60)}`);
214
+ if (roomId) {
215
+ await httpPost(config, `/rooms/${roomId}/messages`, {
216
+ sender_agent_id: config.agentId,
217
+ content: text,
218
+ });
219
+ }
220
+ else {
221
+ await httpPost(config, "/messages/send", {
222
+ from_agent_id: config.agentId,
223
+ to_agent_id: senderId,
224
+ payload: { text },
225
+ });
226
+ }
227
+ console.log(`[openclaw-pincer] ✅ Reply sent to ${senderId.slice(0, 8)}`);
90
228
  }
91
- else {
92
- await httpPost(config, "/messages/send", {
93
- to_agent_id: senderId,
94
- payload: { text: payload.text },
95
- });
229
+ catch (err) {
230
+ console.error(`[openclaw-pincer] Reply failed:`, err.message);
96
231
  }
97
232
  },
98
233
  },
@@ -118,17 +253,19 @@ export const pincerChannel = {
118
253
  config: {
119
254
  listAccountIds: (cfg) => {
120
255
  const c = resolveConfig(cfg);
121
- return c.baseUrl && c.token ? ["default"] : [];
256
+ return c.baseUrl && c.token && c.agentId ? ["default"] : [];
122
257
  },
123
258
  resolveAccount: (_cfg, accountId) => ({ accountId }),
124
259
  },
125
260
  gateway: {
126
261
  startAccount: async (ctx) => {
127
262
  const config = resolveConfig(ctx.cfg);
128
- if (!config.baseUrl || !config.token) {
129
- console.warn("[openclaw-pincer] Missing baseUrl or token. Channel not started.");
263
+ if (!config.baseUrl || !config.token || !config.agentId) {
264
+ console.warn("[openclaw-pincer] Missing baseUrl, token, or agentId. Channel not started.");
130
265
  return;
131
266
  }
267
+ if (!config.agentName)
268
+ config.agentName = "openclaw-agent";
132
269
  const signal = ctx.abortSignal;
133
270
  connectWs({ config, ctx, signal });
134
271
  console.log("[openclaw-pincer] Started, connecting via WebSocket");
@@ -145,10 +282,14 @@ export const pincerChannel = {
145
282
  const config = resolveConfig(ctx.cfg);
146
283
  const to = ctx.to ?? "";
147
284
  if (to.startsWith("room:")) {
148
- await httpPost(config, `/rooms/${to.slice(5)}/messages`, { content: ctx.text });
285
+ await httpPost(config, `/rooms/${to.slice(5)}/messages`, {
286
+ sender_agent_id: config.agentId,
287
+ content: ctx.text,
288
+ });
149
289
  }
150
290
  else {
151
291
  await httpPost(config, "/messages/send", {
292
+ from_agent_id: config.agentId,
152
293
  to_agent_id: to,
153
294
  payload: { text: ctx.text },
154
295
  });
@@ -2,14 +2,16 @@
2
2
  "id": "openclaw-pincer",
3
3
  "name": "Pincer",
4
4
  "description": "Pincer channel plugin for OpenClaw — connects agents to Pincer via WebSocket",
5
- "version": "0.3.0",
5
+ "version": "0.3.1",
6
6
  "channels": ["openclaw-pincer"],
7
7
  "configSchema": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
10
  "properties": {
11
11
  "baseUrl": { "type": "string" },
12
- "token": { "type": "string" }
12
+ "token": { "type": "string" },
13
+ "agentId": { "type": "string" },
14
+ "agentName": { "type": "string" }
13
15
  }
14
16
  }
15
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-pincer",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Pincer channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tsc",
15
- "dev": "tsc --watch"
15
+ "dev": "tsc --watch",
16
+ "prepare": "tsc"
16
17
  },
17
18
  "peerDependencies": {
18
19
  "openclaw": ">=2026.3.0"
@@ -38,4 +39,4 @@
38
39
  "index.ts",
39
40
  "openclaw.plugin.json"
40
41
  ]
41
- }
42
+ }
package/src/channel.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
3
+ * Adapted for Pincer protocol: REGISTER → AUTH → receive envelopes
3
4
  */
4
5
 
5
6
  import WebSocket from "ws";
7
+ import { randomUUID } from "crypto";
6
8
 
7
9
  export interface PincerConfig {
8
10
  baseUrl: string;
9
- token: string;
11
+ token: string; // api_key
12
+ agentId: string; // registered agent UUID on Pincer
13
+ agentName: string; // display name
10
14
  }
11
15
 
12
16
  function resolveConfig(cfg: any): PincerConfig {
@@ -15,14 +19,25 @@ function resolveConfig(cfg: any): PincerConfig {
15
19
 
16
20
  function wsUrl(config: PincerConfig): string {
17
21
  const base = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
18
- return `${base}/api/v1/ws?token=${config.token}`;
22
+ return `${base}/ws?agent_id=${config.agentId}`;
23
+ }
24
+
25
+ function makeEnvelope(type: string, from: string, to: string, payload: any): string {
26
+ return JSON.stringify({
27
+ id: randomUUID(),
28
+ type,
29
+ from,
30
+ to,
31
+ ts: new Date().toISOString(),
32
+ payload,
33
+ });
19
34
  }
20
35
 
21
36
  async function httpPost(config: PincerConfig, path: string, body: any): Promise<void> {
22
37
  const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
23
38
  const res = await fetch(url, {
24
39
  method: "POST",
25
- headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
40
+ headers: { "X-API-Key": config.token, "Content-Type": "application/json" },
26
41
  body: JSON.stringify(body),
27
42
  });
28
43
  if (!res.ok) throw new Error(`Pincer POST ${path} ${res.status}`);
@@ -35,6 +50,7 @@ function connectWs(params: {
35
50
  }) {
36
51
  const { config, ctx, signal } = params;
37
52
  let retryMs = 1000;
53
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
38
54
 
39
55
  function connect() {
40
56
  if (signal.aborted) return;
@@ -43,7 +59,28 @@ function connectWs(params: {
43
59
 
44
60
  ws.on("open", () => {
45
61
  retryMs = 1000;
46
- console.log("[openclaw-pincer] WebSocket connected");
62
+ console.log("[openclaw-pincer] WebSocket connected, sending REGISTER + AUTH");
63
+
64
+ // Pincer handshake: REGISTER then AUTH
65
+ ws.send(makeEnvelope("REGISTER", config.agentId, "hub", {
66
+ name: config.agentName,
67
+ capabilities: [],
68
+ runtime_version: "openclaw/openclaw-pincer/0.3",
69
+ messaging_mode: "ws",
70
+ }));
71
+ ws.send(makeEnvelope("AUTH", config.agentId, "hub", {
72
+ api_key: config.token,
73
+ }));
74
+
75
+ // Heartbeat every 30s
76
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
77
+ heartbeatTimer = setInterval(() => {
78
+ if (ws.readyState === WebSocket.OPEN) {
79
+ ws.send(makeEnvelope("HEARTBEAT", config.agentId, "hub", {
80
+ agent_id: config.agentId,
81
+ }));
82
+ }
83
+ }, 30000);
47
84
  });
48
85
 
49
86
  ws.on("message", (data: WebSocket.Data) => {
@@ -54,6 +91,7 @@ function connectWs(params: {
54
91
  });
55
92
 
56
93
  ws.on("close", () => {
94
+ if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
57
95
  if (signal.aborted) return;
58
96
  console.log(`[openclaw-pincer] WS closed, reconnecting in ${retryMs}ms`);
59
97
  setTimeout(connect, retryMs);
@@ -65,7 +103,10 @@ function connectWs(params: {
65
103
  ws.close();
66
104
  });
67
105
 
68
- signal.addEventListener("abort", () => ws.close(), { once: true });
106
+ signal.addEventListener("abort", () => {
107
+ if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
108
+ ws.close();
109
+ }, { once: true });
69
110
  }
70
111
 
71
112
  connect();
@@ -75,16 +116,106 @@ function handleMessage(config: PincerConfig, ctx: any, msg: any) {
75
116
  const runtime = ctx.channelRuntime;
76
117
  if (!runtime) return;
77
118
 
78
- const sessionKey = msg.room_id
79
- ? `openclaw-pincer:channel:${msg.room_id}`
80
- : `openclaw-pincer:dm:${msg.from_agent_id ?? msg.sender_agent_id ?? "unknown"}`;
119
+ const msgType = msg.type ?? "";
120
+ const payload = msg.payload ?? {};
121
+ const fromId = msg.from ?? "unknown";
81
122
 
82
- const senderId = msg.from_agent_id ?? msg.sender_agent_id ?? "unknown";
83
- const text = msg.content ?? msg.payload?.text ?? "";
84
- if (!text) return;
123
+ // ACK just log
124
+ if (msgType === "ACK") {
125
+ if (payload.status === "ok") {
126
+ console.log("[openclaw-pincer] ✓ Authenticated with Pincer hub");
127
+ } else {
128
+ console.error("[openclaw-pincer] AUTH failed:", payload.error);
129
+ }
130
+ return;
131
+ }
132
+
133
+ // HEARTBEAT_ACK — check for inbox
134
+ if (msgType === "HEARTBEAT_ACK" || msgType === "heartbeat.ack") {
135
+ const inbox = payload.inbox ?? [];
136
+ for (const item of inbox) {
137
+ const inner = item.payload ?? {};
138
+ const text = inner.text ?? JSON.stringify(inner);
139
+ const sender = item.from ?? "unknown";
140
+ dispatchToAgent(config, ctx, runtime, sender, text, undefined);
141
+ }
142
+ return;
143
+ }
144
+
145
+ // Ignore messages from self to prevent echo loops
146
+ if (fromId === config.agentId) {
147
+ return;
148
+ }
149
+
150
+ // PING → PONG
151
+ if (msgType === "PING") {
152
+ // We don't have ws ref here, but heartbeat keeps connection alive
153
+ return;
154
+ }
85
155
 
86
- const peer = msg.room_id
87
- ? { kind: "group" as const, id: msg.room_id }
156
+ // DM message
157
+ if (msgType === "MESSAGE" || msgType === "agent.message") {
158
+ const text = payload.text ?? "";
159
+ if (!text) return;
160
+ console.log(`[openclaw-pincer] 💬 DM from ${fromId.slice(0, 8)}: ${text.slice(0, 60)}`);
161
+ dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
162
+ return;
163
+ }
164
+
165
+ // Inbox delivery (reconnect catch-up)
166
+ if (msgType === "inbox.delivery") {
167
+ const items = Array.isArray(payload) ? payload : [payload];
168
+ for (const item of items) {
169
+ const inner = item.payload ?? {};
170
+ const text = inner.text ?? JSON.stringify(inner);
171
+ const sender = item.from ?? "unknown";
172
+ console.log(`[openclaw-pincer] 📬 Inbox from ${sender.slice(0, 8)}`);
173
+ dispatchToAgent(config, ctx, runtime, sender, text, undefined);
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Room message
179
+ if (msgType === "room.message") {
180
+ const data = msg.data ?? msg.payload ?? {};
181
+ const sender = data.sender_agent_id ?? "unknown";
182
+ const content = data.content ?? "";
183
+ const roomId = data.room_id ?? msg.room_id ?? "";
184
+ if (sender === config.agentId || !content) return;
185
+ console.log(`[openclaw-pincer] 💬 Room msg from ${sender.slice(0, 8)}: ${content.slice(0, 60)}`);
186
+ dispatchToAgent(config, ctx, runtime, sender, content, roomId);
187
+ return;
188
+ }
189
+
190
+ // TASK_ASSIGN
191
+ if (msgType === "TASK_ASSIGN" || msgType === "task.assigned") {
192
+ const taskId = payload.task_id ?? "?";
193
+ const title = payload.title ?? "";
194
+ const description = payload.description ?? "";
195
+ const text = `[Pincer Task]\ntask_id: ${taskId}\ntitle: ${title}\ndescription:\n${description}`;
196
+ console.log(`[openclaw-pincer] 📋 Task assigned: ${taskId.slice(0, 8)} ${title}`);
197
+ dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
198
+ return;
199
+ }
200
+
201
+ // BROADCAST
202
+ if (msgType === "BROADCAST" || msgType === "broadcast") {
203
+ const text = payload.text ?? JSON.stringify(payload);
204
+ console.log(`[openclaw-pincer] 📢 Broadcast: ${text.slice(0, 60)}`);
205
+ return;
206
+ }
207
+ }
208
+
209
+ function dispatchToAgent(
210
+ config: PincerConfig, ctx: any, runtime: any,
211
+ senderId: string, text: string, roomId: string | undefined,
212
+ ) {
213
+ const sessionKey = roomId
214
+ ? `openclaw-pincer:channel:${roomId}`
215
+ : `openclaw-pincer:dm:${senderId}`;
216
+
217
+ const peer = roomId
218
+ ? { kind: "group" as const, id: roomId }
88
219
  : { kind: "direct" as const, id: senderId };
89
220
 
90
221
  const route = runtime.routing.resolveAgentRoute({
@@ -106,13 +237,24 @@ function handleMessage(config: PincerConfig, ctx: any, msg: any) {
106
237
  cfg: ctx.cfg,
107
238
  dispatcherOptions: {
108
239
  deliver: async (payload: any) => {
109
- if (msg.room_id) {
110
- await httpPost(config, `/rooms/${msg.room_id}/messages`, { content: payload.text });
111
- } else {
112
- await httpPost(config, "/messages/send", {
113
- to_agent_id: senderId,
114
- payload: { text: payload.text },
115
- });
240
+ try {
241
+ const text = payload.text ?? payload.content ?? JSON.stringify(payload);
242
+ console.log(`[openclaw-pincer] 📤 Delivering reply to ${senderId.slice(0, 8)}: ${text.slice(0, 60)}`);
243
+ if (roomId) {
244
+ await httpPost(config, `/rooms/${roomId}/messages`, {
245
+ sender_agent_id: config.agentId,
246
+ content: text,
247
+ });
248
+ } else {
249
+ await httpPost(config, "/messages/send", {
250
+ from_agent_id: config.agentId,
251
+ to_agent_id: senderId,
252
+ payload: { text },
253
+ });
254
+ }
255
+ console.log(`[openclaw-pincer] ✅ Reply sent to ${senderId.slice(0, 8)}`);
256
+ } catch (err: any) {
257
+ console.error(`[openclaw-pincer] ❌ Reply failed:`, err.message);
116
258
  }
117
259
  },
118
260
  },
@@ -139,17 +281,18 @@ export const pincerChannel = {
139
281
  config: {
140
282
  listAccountIds: (cfg: any) => {
141
283
  const c = resolveConfig(cfg);
142
- return c.baseUrl && c.token ? ["default"] : [];
284
+ return c.baseUrl && c.token && c.agentId ? ["default"] : [];
143
285
  },
144
286
  resolveAccount: (_cfg: any, accountId: string) => ({ accountId }),
145
287
  },
146
288
  gateway: {
147
289
  startAccount: async (ctx: any) => {
148
290
  const config = resolveConfig(ctx.cfg);
149
- if (!config.baseUrl || !config.token) {
150
- console.warn("[openclaw-pincer] Missing baseUrl or token. Channel not started.");
291
+ if (!config.baseUrl || !config.token || !config.agentId) {
292
+ console.warn("[openclaw-pincer] Missing baseUrl, token, or agentId. Channel not started.");
151
293
  return;
152
294
  }
295
+ if (!config.agentName) config.agentName = "openclaw-agent";
153
296
 
154
297
  const signal: AbortSignal = ctx.abortSignal;
155
298
  connectWs({ config, ctx, signal });
@@ -168,9 +311,13 @@ export const pincerChannel = {
168
311
  const config = resolveConfig(ctx.cfg);
169
312
  const to: string = ctx.to ?? "";
170
313
  if (to.startsWith("room:")) {
171
- await httpPost(config, `/rooms/${to.slice(5)}/messages`, { content: ctx.text });
314
+ await httpPost(config, `/rooms/${to.slice(5)}/messages`, {
315
+ sender_agent_id: config.agentId,
316
+ content: ctx.text,
317
+ });
172
318
  } else {
173
319
  await httpPost(config, "/messages/send", {
320
+ from_agent_id: config.agentId,
174
321
  to_agent_id: to,
175
322
  payload: { text: ctx.text },
176
323
  });