@dhfpub/clawpool-openclaw 0.4.1

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/dist/index.js ADDED
@@ -0,0 +1,4667 @@
1
+ // index.ts
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
3
+
4
+ // src/channel.ts
5
+ import {
6
+ applyAccountNameToChannelSection as applyAccountNameToChannelSection2,
7
+ deleteAccountFromConfigSection,
8
+ formatPairingApproveHint,
9
+ setAccountEnabledInConfigSection
10
+ } from "openclaw/plugin-sdk/core";
11
+ import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
12
+
13
+ // src/account-id.ts
14
+ var DEFAULT_ACCOUNT_ID = "default";
15
+ function normalizeOptionalAccountId(raw) {
16
+ const value = String(raw ?? "").trim();
17
+ if (!value) {
18
+ return void 0;
19
+ }
20
+ return value;
21
+ }
22
+ function normalizeAccountId(raw) {
23
+ return normalizeOptionalAccountId(raw) ?? DEFAULT_ACCOUNT_ID;
24
+ }
25
+
26
+ // src/actions.ts
27
+ import {
28
+ jsonResult,
29
+ readStringParam
30
+ } from "openclaw/plugin-sdk/channel-runtime";
31
+
32
+ // src/accounts.ts
33
+ function rawAibotConfig(cfg) {
34
+ return cfg.channels?.clawpool ?? {};
35
+ }
36
+ function listConfiguredAccountIds(cfg) {
37
+ const accounts = rawAibotConfig(cfg).accounts;
38
+ if (!accounts || typeof accounts !== "object") {
39
+ return [];
40
+ }
41
+ return Object.keys(accounts).filter(Boolean);
42
+ }
43
+ function listAibotAccountIds(cfg) {
44
+ const ids = listConfiguredAccountIds(cfg);
45
+ if (ids.length === 0) {
46
+ return [DEFAULT_ACCOUNT_ID];
47
+ }
48
+ return ids.toSorted((a, b) => a.localeCompare(b));
49
+ }
50
+ function resolveDefaultAibotAccountId(cfg) {
51
+ const aibotCfg = rawAibotConfig(cfg);
52
+ const preferred = normalizeOptionalAccountId(aibotCfg.defaultAccount);
53
+ if (preferred && listAibotAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)) {
54
+ return preferred;
55
+ }
56
+ const ids = listAibotAccountIds(cfg);
57
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
58
+ return DEFAULT_ACCOUNT_ID;
59
+ }
60
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
61
+ }
62
+ function resolveAccountRawConfig(cfg, accountId) {
63
+ const aibotCfg = rawAibotConfig(cfg);
64
+ const { accounts: _ignoredAccounts, defaultAccount: _ignoredDefault, ...base } = aibotCfg;
65
+ const account = aibotCfg.accounts?.[accountId] ?? {};
66
+ return {
67
+ ...base,
68
+ ...account
69
+ };
70
+ }
71
+ function normalizeNonEmpty(value) {
72
+ const s = String(value ?? "").trim();
73
+ return s;
74
+ }
75
+ function normalizeAgentId(value) {
76
+ return normalizeNonEmpty(value);
77
+ }
78
+ function appendAgentIdToWsUrl(rawWsUrl, agentId) {
79
+ if (!rawWsUrl) {
80
+ return "";
81
+ }
82
+ const direct = rawWsUrl.replaceAll("{agent_id}", encodeURIComponent(agentId));
83
+ if (!agentId) {
84
+ return direct;
85
+ }
86
+ try {
87
+ const parsed = new URL(direct);
88
+ if (!parsed.searchParams.get("agent_id")) {
89
+ parsed.searchParams.set("agent_id", agentId);
90
+ }
91
+ return parsed.toString();
92
+ } catch {
93
+ if (direct.includes("agent_id=")) {
94
+ return direct;
95
+ }
96
+ return direct.includes("?") ? `${direct}&agent_id=${encodeURIComponent(agentId)}` : `${direct}?agent_id=${encodeURIComponent(agentId)}`;
97
+ }
98
+ }
99
+ function resolveWsUrl(merged, agentId) {
100
+ const envWs = normalizeNonEmpty(process.env.CLAWPOOL_WS_URL);
101
+ const cfgWs = normalizeNonEmpty(merged.wsUrl);
102
+ const ws = cfgWs || envWs;
103
+ if (ws) {
104
+ return appendAgentIdToWsUrl(ws, agentId);
105
+ }
106
+ if (!agentId) {
107
+ return "";
108
+ }
109
+ return `ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=${encodeURIComponent(agentId)}`;
110
+ }
111
+ function redactAibotWsUrl(wsUrl) {
112
+ if (!wsUrl) {
113
+ return "";
114
+ }
115
+ try {
116
+ const parsed = new URL(wsUrl);
117
+ if (parsed.searchParams.has("agent_id")) {
118
+ parsed.searchParams.set("agent_id", "***");
119
+ }
120
+ return parsed.toString();
121
+ } catch {
122
+ return wsUrl.replace(/(agent_id=)[^&]+/g, "$1***");
123
+ }
124
+ }
125
+ function resolveAibotAccount(params) {
126
+ const accountId = normalizeAccountId(params.accountId);
127
+ const merged = resolveAccountRawConfig(params.cfg, accountId);
128
+ const baseEnabled = rawAibotConfig(params.cfg).enabled !== false;
129
+ const accountEnabled = merged.enabled !== false;
130
+ const enabled = baseEnabled && accountEnabled;
131
+ const agentId = normalizeAgentId(merged.agentId || process.env.CLAWPOOL_AGENT_ID);
132
+ const apiKey = normalizeNonEmpty(merged.apiKey || process.env.CLAWPOOL_API_KEY);
133
+ const wsUrl = resolveWsUrl(merged, agentId);
134
+ const configured = Boolean(wsUrl && agentId && apiKey);
135
+ return {
136
+ accountId,
137
+ name: normalizeNonEmpty(merged.name) || void 0,
138
+ enabled,
139
+ configured,
140
+ wsUrl,
141
+ agentId,
142
+ apiKey,
143
+ config: merged
144
+ };
145
+ }
146
+ function normalizeAibotSessionTarget(raw) {
147
+ const trimmed = String(raw ?? "").trim();
148
+ if (!trimmed) {
149
+ return "";
150
+ }
151
+ return trimmed.replace(/^clawpool:/i, "").replace(/^session:/i, "").trim();
152
+ }
153
+
154
+ // src/client.ts
155
+ import { randomUUID } from "node:crypto";
156
+
157
+ // src/protocol-send.ts
158
+ var AIBOT_PROTOCOL_SEND_RATE_LIMIT = 8;
159
+ var AIBOT_PROTOCOL_SEND_RATE_WINDOW_MS = 1e4;
160
+ var AIBOT_PROTOCOL_SEND_RETRYABLE_CODE = 4008;
161
+ var AIBOT_PROTOCOL_SEND_RETRY_MAX_ATTEMPTS = 3;
162
+ var AIBOT_PROTOCOL_SEND_RETRY_BASE_DELAY_MS = 600;
163
+ var AIBOT_PROTOCOL_SEND_RETRY_MAX_DELAY_MS = 2e3;
164
+ var AIBOT_PROTOCOL_SEND_RATE_SAFETY_DELAY_MS = 100;
165
+ function isRetryableAibotSendCode(code) {
166
+ return Number(code) === AIBOT_PROTOCOL_SEND_RETRYABLE_CODE;
167
+ }
168
+ function resolveAibotSendRetryMaxAttempts() {
169
+ return AIBOT_PROTOCOL_SEND_RETRY_MAX_ATTEMPTS;
170
+ }
171
+ function resolveAibotSendRetryDelayMs(attempt) {
172
+ const normalizedAttempt = Math.max(1, Math.floor(attempt));
173
+ const multiplier = 2 ** Math.max(0, normalizedAttempt - 1);
174
+ return Math.min(
175
+ AIBOT_PROTOCOL_SEND_RETRY_MAX_DELAY_MS,
176
+ AIBOT_PROTOCOL_SEND_RETRY_BASE_DELAY_MS * multiplier
177
+ );
178
+ }
179
+ function pruneAibotSendWindow(sentAtMs, nowMs) {
180
+ return sentAtMs.filter((value) => nowMs - value < AIBOT_PROTOCOL_SEND_RATE_WINDOW_MS);
181
+ }
182
+ function computeAibotSendThrottleDelayMs(sentAtMs, nowMs) {
183
+ const recent = pruneAibotSendWindow(sentAtMs, nowMs);
184
+ if (recent.length < AIBOT_PROTOCOL_SEND_RATE_LIMIT) {
185
+ return 0;
186
+ }
187
+ const earliest = recent[0] ?? nowMs;
188
+ return Math.max(1, earliest + AIBOT_PROTOCOL_SEND_RATE_WINDOW_MS - nowMs + AIBOT_PROTOCOL_SEND_RATE_SAFETY_DELAY_MS);
189
+ }
190
+
191
+ // src/protocol-text.ts
192
+ var AIBOT_PROTOCOL_MAX_RUNES = 2e3;
193
+ var AIBOT_PROTOCOL_MAX_BYTES = 12 * 1024;
194
+ var DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT = 1200;
195
+ var DEFAULT_STREAM_CHUNK_LIMIT = 48;
196
+ function clampPositiveInt(value, fallback) {
197
+ const parsed = Number(value);
198
+ if (!Number.isFinite(parsed)) {
199
+ return fallback;
200
+ }
201
+ return Math.max(1, Math.floor(parsed));
202
+ }
203
+ function resolveOutboundTextChunkLimit(value) {
204
+ return Math.min(
205
+ AIBOT_PROTOCOL_MAX_RUNES,
206
+ clampPositiveInt(value, DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT)
207
+ );
208
+ }
209
+ function resolveStreamTextChunkLimit(value) {
210
+ return Math.min(
211
+ AIBOT_PROTOCOL_MAX_RUNES,
212
+ clampPositiveInt(value, DEFAULT_STREAM_CHUNK_LIMIT)
213
+ );
214
+ }
215
+ function splitTextForAibotProtocol(text, preferredRunes) {
216
+ const source = String(text ?? "");
217
+ if (!source) {
218
+ return [];
219
+ }
220
+ const runeLimit = Math.min(AIBOT_PROTOCOL_MAX_RUNES, Math.max(1, Math.floor(preferredRunes)));
221
+ const chunks = [];
222
+ let current = "";
223
+ let currentRunes = 0;
224
+ let currentBytes = 0;
225
+ for (const rune of source) {
226
+ const runeBytes = Buffer.byteLength(rune, "utf8");
227
+ const nextRunes = currentRunes + 1;
228
+ const nextBytes = currentBytes + runeBytes;
229
+ const exceedPreferredRunes = nextRunes > runeLimit;
230
+ const exceedProtocolBytes = nextBytes > AIBOT_PROTOCOL_MAX_BYTES;
231
+ if (current && (exceedPreferredRunes || exceedProtocolBytes)) {
232
+ chunks.push(current);
233
+ current = "";
234
+ currentRunes = 0;
235
+ currentBytes = 0;
236
+ }
237
+ current += rune;
238
+ currentRunes += 1;
239
+ currentBytes += runeBytes;
240
+ }
241
+ if (current) {
242
+ chunks.push(current);
243
+ }
244
+ return chunks;
245
+ }
246
+
247
+ // src/client.ts
248
+ var DEFAULT_RECONNECT_BASE_MS = 2e3;
249
+ var DEFAULT_RECONNECT_MAX_MS = 3e4;
250
+ var DEFAULT_RECONNECT_STABLE_MS = 3e4;
251
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
252
+ var DEFAULT_HEARTBEAT_SEC = 30;
253
+ function buildAuthPayload(account) {
254
+ return {
255
+ agent_id: account.agentId,
256
+ api_key: account.apiKey,
257
+ client: "openclaw-clawpool",
258
+ client_type: "openclaw"
259
+ };
260
+ }
261
+ var AibotPacketError = class extends Error {
262
+ cmd;
263
+ code;
264
+ constructor(cmd, code, message) {
265
+ super(`clawpool ${cmd}: code=${code} msg=${message}`);
266
+ this.name = "AibotPacketError";
267
+ this.cmd = cmd;
268
+ this.code = code;
269
+ }
270
+ };
271
+ function clampInt(value, fallback, min, max) {
272
+ const n = Number(value);
273
+ if (!Number.isFinite(n)) {
274
+ return fallback;
275
+ }
276
+ return Math.max(min, Math.min(max, Math.floor(n)));
277
+ }
278
+ function buildFastRetryDelays(baseDelayMs) {
279
+ const first = Math.max(100, Math.min(300, Math.floor(baseDelayMs / 4)));
280
+ const second = Math.max(first, Math.min(1e3, Math.floor(baseDelayMs / 2)));
281
+ return [first, second];
282
+ }
283
+ function randomIntInclusive(min, max) {
284
+ const boundedMin = Math.floor(min);
285
+ const boundedMax = Math.floor(max);
286
+ if (boundedMax <= boundedMin) {
287
+ return boundedMin;
288
+ }
289
+ return boundedMin + Math.floor(Math.random() * (boundedMax - boundedMin + 1));
290
+ }
291
+ function normalizeCloseReason(value) {
292
+ const reason = String(value ?? "").replace(/\s+/g, " ").trim();
293
+ if (!reason) {
294
+ return void 0;
295
+ }
296
+ return reason.slice(0, 160);
297
+ }
298
+ function redactWsUrlForLog(wsUrl) {
299
+ if (!wsUrl) {
300
+ return "";
301
+ }
302
+ try {
303
+ const parsed = new URL(wsUrl);
304
+ if (parsed.searchParams.has("agent_id")) {
305
+ parsed.searchParams.set("agent_id", "***");
306
+ }
307
+ return parsed.toString();
308
+ } catch {
309
+ return wsUrl.replace(/(agent_id=)[^&]+/g, "$1***");
310
+ }
311
+ }
312
+ function parseHeartbeatSec(payload) {
313
+ return clampInt(payload.heartbeat_sec, DEFAULT_HEARTBEAT_SEC, 5, 300);
314
+ }
315
+ async function sleepWithAbort(ms, abortSignal) {
316
+ if (ms <= 0 || abortSignal.aborted) {
317
+ return;
318
+ }
319
+ await new Promise((resolve) => {
320
+ let settled = false;
321
+ let timer = null;
322
+ function finish() {
323
+ if (settled) {
324
+ return;
325
+ }
326
+ settled = true;
327
+ if (timer) {
328
+ clearTimeout(timer);
329
+ }
330
+ abortSignal.removeEventListener("abort", onAbort);
331
+ resolve();
332
+ }
333
+ function onAbort() {
334
+ finish();
335
+ }
336
+ timer = setTimeout(finish, ms);
337
+ abortSignal.addEventListener("abort", onAbort, { once: true });
338
+ });
339
+ }
340
+ async function sleep(ms) {
341
+ if (ms <= 0) {
342
+ return;
343
+ }
344
+ await new Promise((resolve) => {
345
+ setTimeout(resolve, ms);
346
+ });
347
+ }
348
+ function resolveReconnectPolicy(account) {
349
+ const baseDelayMs = clampInt(account.config.reconnectMs, DEFAULT_RECONNECT_BASE_MS, 100, 6e4);
350
+ const fallbackMaxMs = Math.max(DEFAULT_RECONNECT_MAX_MS, baseDelayMs * 8);
351
+ const maxDelayMs = clampInt(account.config.reconnectMaxMs, fallbackMaxMs, baseDelayMs, 3e5);
352
+ const stableConnectionMs = clampInt(
353
+ account.config.reconnectStableMs,
354
+ DEFAULT_RECONNECT_STABLE_MS,
355
+ 1e3,
356
+ 6e5
357
+ );
358
+ const connectTimeoutMs = clampInt(
359
+ account.config.connectTimeoutMs,
360
+ DEFAULT_CONNECT_TIMEOUT_MS,
361
+ 1e3,
362
+ 6e4
363
+ );
364
+ const fastRetryDelaysMs = buildFastRetryDelays(baseDelayMs);
365
+ return {
366
+ baseDelayMs,
367
+ maxDelayMs,
368
+ stableConnectionMs,
369
+ fastRetryDelaysMs,
370
+ authPenaltyAttemptFloor: fastRetryDelaysMs.length + 4,
371
+ connectTimeoutMs
372
+ };
373
+ }
374
+ var AuthRejectedError = class extends Error {
375
+ code;
376
+ constructor(code, message) {
377
+ super(`clawpool auth failed: code=${code}, msg=${message}`);
378
+ this.name = "AuthRejectedError";
379
+ this.code = code;
380
+ }
381
+ };
382
+ function parseCode(payload) {
383
+ const n = Number(payload.code ?? 0);
384
+ if (Number.isFinite(n)) {
385
+ return n;
386
+ }
387
+ return 0;
388
+ }
389
+ function parseMessage(payload) {
390
+ const s = String(payload.msg ?? "").trim();
391
+ return s || "unknown error";
392
+ }
393
+ function parseKickedReason(payload) {
394
+ const reason = String(payload.reason ?? payload.msg ?? "").trim();
395
+ return reason || "unknown";
396
+ }
397
+ async function wsDataToText(data) {
398
+ if (typeof data === "string") {
399
+ return data;
400
+ }
401
+ if (data instanceof ArrayBuffer) {
402
+ return Buffer.from(data).toString("utf8");
403
+ }
404
+ if (ArrayBuffer.isView(data)) {
405
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
406
+ }
407
+ if (data && typeof data.text === "function") {
408
+ return data.text();
409
+ }
410
+ return String(data ?? "");
411
+ }
412
+ var AibotWsClient = class {
413
+ account;
414
+ callbacks;
415
+ reconnectPolicy;
416
+ ws = null;
417
+ running = false;
418
+ seq = Date.now();
419
+ loopPromise = null;
420
+ pending = /* @__PURE__ */ new Map();
421
+ pendingStreamHighSurrogate = /* @__PURE__ */ new Map();
422
+ sendMsgWindowBySession = /* @__PURE__ */ new Map();
423
+ reconnectPenaltyAttemptFloor = 0;
424
+ connectionSerial = 0;
425
+ activeConnectionSerial = 0;
426
+ keepaliveTimer = null;
427
+ keepaliveInFlight = false;
428
+ lastConnectionError = "";
429
+ lastConnectionErrorLogAt = 0;
430
+ suppressedConnectionErrors = 0;
431
+ lastReconnectLogAt = 0;
432
+ suppressedReconnectLogs = 0;
433
+ status = {
434
+ running: false,
435
+ connected: false,
436
+ authed: false,
437
+ lastError: null,
438
+ lastConnectAt: null,
439
+ lastDisconnectAt: null
440
+ };
441
+ constructor(account, callbacks = {}) {
442
+ this.account = account;
443
+ this.callbacks = callbacks;
444
+ this.reconnectPolicy = resolveReconnectPolicy(account);
445
+ }
446
+ logInfo(message) {
447
+ this.callbacks.logger?.info?.(`[clawpool] [${this.account.accountId}] ${message}`);
448
+ }
449
+ logWarn(message) {
450
+ this.callbacks.logger?.warn?.(`[clawpool] [${this.account.accountId}] ${message}`);
451
+ }
452
+ logError(message) {
453
+ this.callbacks.logger?.error?.(`[clawpool] [${this.account.accountId}] ${message}`);
454
+ }
455
+ logConnectionError(message) {
456
+ const now = Date.now();
457
+ const sameAsLast = this.lastConnectionError === message;
458
+ const shouldLog = !sameAsLast || now - this.lastConnectionErrorLogAt >= 3e4 || this.suppressedConnectionErrors >= 10;
459
+ if (!shouldLog) {
460
+ this.suppressedConnectionErrors += 1;
461
+ return;
462
+ }
463
+ const repeats = this.suppressedConnectionErrors;
464
+ this.lastConnectionError = message;
465
+ this.lastConnectionErrorLogAt = now;
466
+ this.suppressedConnectionErrors = 0;
467
+ if (repeats > 0) {
468
+ this.logWarn(`connection error: ${message} (suppressed=${repeats})`);
469
+ return;
470
+ }
471
+ this.logWarn(`connection error: ${message}`);
472
+ }
473
+ logReconnectPlan(params) {
474
+ const now = Date.now();
475
+ const important = params.attempt <= 3 || params.authRejected || params.penaltyFloor > 0 || params.stable || params.attempt % 10 === 0;
476
+ const shouldLog = important || now - this.lastReconnectLogAt >= 3e4;
477
+ if (!shouldLog) {
478
+ this.suppressedReconnectLogs += 1;
479
+ return;
480
+ }
481
+ const suppressed = this.suppressedReconnectLogs;
482
+ this.suppressedReconnectLogs = 0;
483
+ this.lastReconnectLogAt = now;
484
+ this.logInfo(
485
+ `reconnect scheduled in ${params.delayMs}ms attempt=${params.attempt} stable=${params.stable} authRejected=${params.authRejected} penaltyFloor=${params.penaltyFloor} suppressed=${suppressed}`
486
+ );
487
+ }
488
+ getStatus() {
489
+ return { ...this.status };
490
+ }
491
+ async start(abortSignal) {
492
+ if (this.running) {
493
+ return;
494
+ }
495
+ this.running = true;
496
+ this.updateStatus({ running: true, lastError: null });
497
+ this.logInfo(
498
+ `client start ws=${redactWsUrlForLog(this.account.wsUrl)} reconnectBaseMs=${this.reconnectPolicy.baseDelayMs} reconnectMaxMs=${this.reconnectPolicy.maxDelayMs} reconnectStableMs=${this.reconnectPolicy.stableConnectionMs} connectTimeoutMs=${this.reconnectPolicy.connectTimeoutMs}`
499
+ );
500
+ this.loopPromise = this.runLoop(abortSignal);
501
+ void this.loopPromise.catch((err) => {
502
+ const msg = err instanceof Error ? err.message : String(err);
503
+ this.updateStatus({
504
+ running: false,
505
+ connected: false,
506
+ authed: false,
507
+ lastError: msg,
508
+ lastDisconnectAt: Date.now()
509
+ });
510
+ this.logError(`run loop crashed: ${msg}`);
511
+ });
512
+ }
513
+ stop() {
514
+ this.running = false;
515
+ this.stopKeepalive();
516
+ this.rejectAllPending(new Error("clawpool client stopped"));
517
+ this.safeCloseWs("client_stopped");
518
+ this.updateStatus({
519
+ running: false,
520
+ connected: false,
521
+ authed: false,
522
+ lastDisconnectAt: Date.now()
523
+ });
524
+ }
525
+ async waitUntilStopped() {
526
+ await this.loopPromise;
527
+ }
528
+ async sendText(sessionId, text, opts = {}) {
529
+ this.ensureReady();
530
+ const clientMsgId = opts.clientMsgId || `clawpool_${randomUUID()}`;
531
+ const payload = this.buildSendTextPayload(sessionId, text, clientMsgId, opts);
532
+ try {
533
+ return await this.sendMessageWithRetry(sessionId, payload, opts.timeoutMs ?? 2e4, "sendText");
534
+ } catch (err) {
535
+ if (!this.isMessageTooLargeError(err)) {
536
+ throw err;
537
+ }
538
+ return this.sendSplitTextAfterSizeError(sessionId, text, clientMsgId, opts);
539
+ }
540
+ }
541
+ async sendMedia(sessionId, mediaUrl, caption = "", opts = {}) {
542
+ this.ensureReady();
543
+ const clientMsgId = opts.clientMsgId || `clawpool_${randomUUID()}`;
544
+ const payload = this.buildSendMediaPayload(sessionId, mediaUrl, caption, clientMsgId, opts);
545
+ try {
546
+ return await this.sendMessageWithRetry(sessionId, payload, opts.timeoutMs ?? 3e4, "sendMedia");
547
+ } catch (err) {
548
+ if (!this.isMessageTooLargeError(err) || !caption) {
549
+ throw err;
550
+ }
551
+ return this.sendMediaCaptionAfterSizeError(sessionId, mediaUrl, caption, clientMsgId, opts);
552
+ }
553
+ }
554
+ async bindSessionRoute(channel, accountId, routeSessionKey, sessionId, opts = {}) {
555
+ this.ensureReady();
556
+ const normalizedChannel = String(channel ?? "").trim().toLowerCase();
557
+ const normalizedAccountID = String(accountId ?? "").trim();
558
+ const normalizedRouteSessionKey = String(routeSessionKey ?? "").trim();
559
+ const normalizedSessionID = String(sessionId ?? "").trim();
560
+ if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey || !normalizedSessionID) {
561
+ throw new Error("clawpool session_route_bind requires channel/account_id/route_session_key/session_id");
562
+ }
563
+ this.logInfo(
564
+ `session_route_bind request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
565
+ );
566
+ const packet = await this.request(
567
+ "session_route_bind",
568
+ {
569
+ channel: normalizedChannel,
570
+ account_id: normalizedAccountID,
571
+ route_session_key: normalizedRouteSessionKey,
572
+ session_id: normalizedSessionID
573
+ },
574
+ {
575
+ expected: ["send_ack", "send_nack", "error"],
576
+ timeoutMs: opts.timeoutMs ?? 1e4
577
+ }
578
+ );
579
+ if (packet.cmd !== "send_ack") {
580
+ this.logWarn(
581
+ `session_route_bind nack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
582
+ );
583
+ throw this.packetError(packet);
584
+ }
585
+ this.logInfo(
586
+ `session_route_bind ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
587
+ );
588
+ return packet.payload;
589
+ }
590
+ async resolveSessionRoute(channel, accountId, routeSessionKey, opts = {}) {
591
+ this.ensureReady();
592
+ const normalizedChannel = String(channel ?? "").trim().toLowerCase();
593
+ const normalizedAccountID = String(accountId ?? "").trim();
594
+ const normalizedRouteSessionKey = String(routeSessionKey ?? "").trim();
595
+ if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey) {
596
+ throw new Error("clawpool session_route_resolve requires channel/account_id/route_session_key");
597
+ }
598
+ this.logInfo(
599
+ `session_route_resolve request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
600
+ );
601
+ const packet = await this.request(
602
+ "session_route_resolve",
603
+ {
604
+ channel: normalizedChannel,
605
+ account_id: normalizedAccountID,
606
+ route_session_key: normalizedRouteSessionKey
607
+ },
608
+ {
609
+ expected: ["send_ack", "send_nack", "error"],
610
+ timeoutMs: opts.timeoutMs ?? 1e4
611
+ }
612
+ );
613
+ if (packet.cmd !== "send_ack") {
614
+ this.logWarn(
615
+ `session_route_resolve nack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
616
+ );
617
+ throw this.packetError(packet);
618
+ }
619
+ const payload = packet.payload;
620
+ const normalizedSessionID = String(payload.session_id ?? "").trim();
621
+ if (!normalizedSessionID) {
622
+ throw new Error("clawpool session_route_resolve ack missing session_id");
623
+ }
624
+ this.logInfo(
625
+ `session_route_resolve ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
626
+ );
627
+ return {
628
+ ...payload,
629
+ channel: String(payload.channel ?? normalizedChannel),
630
+ account_id: String(payload.account_id ?? normalizedAccountID),
631
+ route_session_key: String(payload.route_session_key ?? normalizedRouteSessionKey),
632
+ session_id: normalizedSessionID
633
+ };
634
+ }
635
+ async sendStreamChunk(sessionId, deltaContent, opts) {
636
+ this.ensureReady();
637
+ const normalizedDeltaContent = this.normalizeStreamDeltaContent(
638
+ opts.clientMsgId,
639
+ deltaContent,
640
+ opts.isFinish === true
641
+ );
642
+ if (!normalizedDeltaContent && !opts.isFinish) {
643
+ return;
644
+ }
645
+ const payload = {
646
+ session_id: sessionId,
647
+ client_msg_id: opts.clientMsgId,
648
+ delta_content: normalizedDeltaContent,
649
+ is_finish: opts.isFinish ?? false
650
+ };
651
+ const eventId = String(opts.eventId ?? "").trim();
652
+ if (eventId) {
653
+ payload.event_id = eventId;
654
+ }
655
+ if (opts.quotedMessageId) {
656
+ payload.quoted_message_id = opts.quotedMessageId;
657
+ }
658
+ if (opts.isFinish) {
659
+ const packet = await this.request("client_stream_chunk", payload, {
660
+ expected: ["send_ack", "send_nack", "error"],
661
+ timeoutMs: opts.timeoutMs ?? 2e4
662
+ });
663
+ if (packet.cmd !== "send_ack") {
664
+ throw this.packetError(packet);
665
+ }
666
+ return packet.payload;
667
+ }
668
+ this.sendPacket("client_stream_chunk", payload);
669
+ }
670
+ async deleteMessage(sessionId, msgId, opts = {}) {
671
+ this.ensureReady();
672
+ const normalizedSessionId = String(sessionId ?? "").trim();
673
+ if (!normalizedSessionId) {
674
+ throw new Error("clawpool delete_msg requires session_id");
675
+ }
676
+ const normalizedMsgId = String(msgId ?? "").trim();
677
+ if (!/^\d+$/.test(normalizedMsgId)) {
678
+ throw new Error("clawpool delete_msg requires numeric msg_id");
679
+ }
680
+ const packet = await this.request(
681
+ "delete_msg",
682
+ {
683
+ session_id: normalizedSessionId,
684
+ msg_id: normalizedMsgId
685
+ },
686
+ {
687
+ expected: ["send_ack", "send_nack", "error"],
688
+ timeoutMs: opts.timeoutMs ?? 2e4
689
+ }
690
+ );
691
+ if (packet.cmd !== "send_ack") {
692
+ throw this.packetError(packet);
693
+ }
694
+ return packet.payload;
695
+ }
696
+ ackEvent(eventId, payload = {}) {
697
+ this.ensureReady();
698
+ const normalizedEventId = String(eventId ?? "").trim();
699
+ if (!normalizedEventId) {
700
+ throw new Error("clawpool event_ack requires event_id");
701
+ }
702
+ const ackPayload = {
703
+ event_id: normalizedEventId,
704
+ received_at: Math.floor(payload.receivedAt ?? Date.now())
705
+ };
706
+ const sessionId = String(payload.sessionId ?? "").trim();
707
+ if (sessionId) {
708
+ ackPayload.session_id = sessionId;
709
+ }
710
+ const msgId = String(payload.msgId ?? "").trim();
711
+ if (/^\d+$/.test(msgId)) {
712
+ ackPayload.msg_id = msgId;
713
+ }
714
+ this.sendPacket("event_ack", ackPayload);
715
+ }
716
+ sendEventResult(payload) {
717
+ this.ensureReady();
718
+ const eventId = String(payload.event_id ?? "").trim();
719
+ const status = String(payload.status ?? "").trim();
720
+ if (!eventId) {
721
+ throw new Error("clawpool event_result requires event_id");
722
+ }
723
+ if (!status) {
724
+ throw new Error("clawpool event_result requires status");
725
+ }
726
+ const packet = {
727
+ event_id: eventId,
728
+ status
729
+ };
730
+ const code = String(payload.code ?? "").trim();
731
+ if (code) {
732
+ packet.code = code;
733
+ }
734
+ const msg = String(payload.msg ?? "").trim();
735
+ if (msg) {
736
+ packet.msg = msg;
737
+ }
738
+ const updatedAt = Number(payload.updated_at);
739
+ if (Number.isFinite(updatedAt) && updatedAt > 0) {
740
+ packet.updated_at = Math.floor(updatedAt);
741
+ }
742
+ this.sendPacket("event_result", packet);
743
+ }
744
+ sendEventStopAck(payload) {
745
+ this.ensureReady();
746
+ const eventId = String(payload.event_id ?? "").trim();
747
+ if (!eventId) {
748
+ throw new Error("clawpool event_stop_ack requires event_id");
749
+ }
750
+ const packet = {
751
+ event_id: eventId,
752
+ accepted: payload.accepted === true
753
+ };
754
+ const stopId = String(payload.stop_id ?? "").trim();
755
+ if (stopId) {
756
+ packet.stop_id = stopId;
757
+ }
758
+ const updatedAt = Number(payload.updated_at);
759
+ if (Number.isFinite(updatedAt) && updatedAt > 0) {
760
+ packet.updated_at = Math.floor(updatedAt);
761
+ }
762
+ this.sendPacket("event_stop_ack", packet);
763
+ }
764
+ sendEventStopResult(payload) {
765
+ this.ensureReady();
766
+ const eventId = String(payload.event_id ?? "").trim();
767
+ const status = String(payload.status ?? "").trim();
768
+ if (!eventId) {
769
+ throw new Error("clawpool event_stop_result requires event_id");
770
+ }
771
+ if (!status) {
772
+ throw new Error("clawpool event_stop_result requires status");
773
+ }
774
+ const packet = {
775
+ event_id: eventId,
776
+ status
777
+ };
778
+ const stopId = String(payload.stop_id ?? "").trim();
779
+ if (stopId) {
780
+ packet.stop_id = stopId;
781
+ }
782
+ const code = String(payload.code ?? "").trim();
783
+ if (code) {
784
+ packet.code = code;
785
+ }
786
+ const msg = String(payload.msg ?? "").trim();
787
+ if (msg) {
788
+ packet.msg = msg;
789
+ }
790
+ const updatedAt = Number(payload.updated_at);
791
+ if (Number.isFinite(updatedAt) && updatedAt > 0) {
792
+ packet.updated_at = Math.floor(updatedAt);
793
+ }
794
+ this.sendPacket("event_stop_result", packet);
795
+ }
796
+ setSessionComposing(sessionId, active, opts = {}) {
797
+ this.ensureReady();
798
+ const normalizedSessionId = String(sessionId ?? "").trim();
799
+ if (!normalizedSessionId) {
800
+ throw new Error("clawpool session_activity_set requires session_id");
801
+ }
802
+ const payload = {
803
+ session_id: normalizedSessionId,
804
+ kind: "composing",
805
+ active
806
+ };
807
+ const refEventId = String(opts.refEventId ?? "").trim();
808
+ if (refEventId) {
809
+ payload.ref_event_id = refEventId;
810
+ }
811
+ const refMsgId = String(opts.refMsgId ?? "").trim();
812
+ if (/^\d+$/.test(refMsgId)) {
813
+ payload.ref_msg_id = refMsgId;
814
+ }
815
+ this.sendPacket("session_activity_set", payload);
816
+ }
817
+ async runLoop(abortSignal) {
818
+ let attempt = 0;
819
+ while (this.running && !abortSignal.aborted) {
820
+ let uptimeMs = 0;
821
+ let authRejected = false;
822
+ let shouldReconnect = true;
823
+ const cycle = attempt + 1;
824
+ try {
825
+ const outcome = await this.connectOnce(abortSignal, cycle);
826
+ uptimeMs = outcome.uptimeMs;
827
+ shouldReconnect = !outcome.aborted;
828
+ if (!outcome.aborted) {
829
+ const codeText = outcome.closeCode != null ? String(outcome.closeCode) : "-";
830
+ const reasonText = outcome.closeReason ? ` reason=${outcome.closeReason}` : "";
831
+ this.logWarn(
832
+ `websocket closed cause=${outcome.cause} code=${codeText}${reasonText} uptimeMs=${uptimeMs}`
833
+ );
834
+ }
835
+ } catch (err) {
836
+ const msg = err instanceof Error ? err.message : String(err);
837
+ authRejected = err instanceof AuthRejectedError;
838
+ this.updateStatus({
839
+ connected: false,
840
+ authed: false,
841
+ lastError: msg,
842
+ lastDisconnectAt: Date.now()
843
+ });
844
+ this.logConnectionError(msg);
845
+ }
846
+ if (!this.running || abortSignal.aborted || !shouldReconnect) {
847
+ break;
848
+ }
849
+ const stable = uptimeMs >= this.reconnectPolicy.stableConnectionMs;
850
+ if (stable) {
851
+ attempt = 0;
852
+ }
853
+ attempt += 1;
854
+ if (authRejected) {
855
+ attempt = Math.max(attempt, this.reconnectPolicy.authPenaltyAttemptFloor);
856
+ }
857
+ const penaltyFloor = this.consumeReconnectPenaltyAttemptFloor();
858
+ if (penaltyFloor > 0) {
859
+ attempt = Math.max(attempt, penaltyFloor);
860
+ }
861
+ const delay = this.resolveReconnectDelayMs(attempt);
862
+ this.logReconnectPlan({
863
+ delayMs: delay,
864
+ attempt,
865
+ stable,
866
+ authRejected,
867
+ penaltyFloor
868
+ });
869
+ await sleepWithAbort(delay, abortSignal);
870
+ }
871
+ this.stop();
872
+ }
873
+ async connectOnce(abortSignal, cycle) {
874
+ const connSerial = this.nextConnectionSerial();
875
+ this.logInfo(`websocket connect begin conn=${connSerial} cycle=${cycle}`);
876
+ const ws = await this.openWebSocket(this.account.wsUrl, abortSignal);
877
+ this.ws = ws;
878
+ this.activeConnectionSerial = connSerial;
879
+ const connectedAt = Date.now();
880
+ this.updateStatus({
881
+ connected: true,
882
+ authed: false,
883
+ lastError: null,
884
+ lastConnectAt: connectedAt
885
+ });
886
+ this.logInfo(`websocket connected conn=${connSerial}`);
887
+ const onMessage = (event) => {
888
+ void this.handleMessageEvent(event.data, connSerial).catch((err) => {
889
+ const message = err instanceof Error ? err.message : String(err);
890
+ this.logError(`handle message failed conn=${connSerial}: ${message}`);
891
+ });
892
+ };
893
+ const onClose = () => {
894
+ this.stopKeepalive();
895
+ this.updateStatus({
896
+ connected: false,
897
+ authed: false,
898
+ lastDisconnectAt: Date.now()
899
+ });
900
+ this.rejectAllPending(new Error("clawpool websocket closed"));
901
+ if (this.ws === ws && ws.readyState !== WebSocket.OPEN) {
902
+ this.ws = null;
903
+ if (this.activeConnectionSerial === connSerial) {
904
+ this.activeConnectionSerial = 0;
905
+ }
906
+ }
907
+ };
908
+ const onError = () => {
909
+ this.stopKeepalive();
910
+ this.updateStatus({
911
+ connected: false,
912
+ authed: false,
913
+ lastDisconnectAt: Date.now()
914
+ });
915
+ this.rejectAllPending(new Error("clawpool websocket error"));
916
+ };
917
+ ws.addEventListener("message", onMessage);
918
+ ws.addEventListener("close", onClose);
919
+ ws.addEventListener("error", onError);
920
+ try {
921
+ const auth = await this.authenticate(connSerial);
922
+ this.startKeepalive(ws, connSerial, auth.heartbeatSec);
923
+ const outcome = await this.waitForCloseOrAbort(ws, abortSignal);
924
+ return {
925
+ ...outcome,
926
+ uptimeMs: Math.max(0, Date.now() - connectedAt)
927
+ };
928
+ } catch (err) {
929
+ this.safeCloseSpecificWs(ws, "connect_once_error");
930
+ throw err;
931
+ } finally {
932
+ ws.removeEventListener("message", onMessage);
933
+ ws.removeEventListener("close", onClose);
934
+ ws.removeEventListener("error", onError);
935
+ this.stopKeepalive();
936
+ this.safeCloseSpecificWs(ws, "connect_once_finally");
937
+ if (this.activeConnectionSerial === connSerial) {
938
+ this.activeConnectionSerial = 0;
939
+ }
940
+ this.logInfo(`websocket connect end conn=${connSerial}`);
941
+ }
942
+ }
943
+ async openWebSocket(url, abortSignal) {
944
+ return new Promise((resolve, reject) => {
945
+ const ws = new WebSocket(url);
946
+ let done = false;
947
+ const timeoutMs = this.reconnectPolicy.connectTimeoutMs;
948
+ let timer = null;
949
+ const closeWs = () => {
950
+ try {
951
+ ws.close();
952
+ } catch {
953
+ }
954
+ };
955
+ const onOpen = () => {
956
+ finish(() => resolve(ws));
957
+ };
958
+ const onError = () => {
959
+ finish(() => reject(new Error("clawpool websocket connect failed")));
960
+ };
961
+ const onAbort = () => {
962
+ finish(() => {
963
+ closeWs();
964
+ reject(new Error("aborted"));
965
+ });
966
+ };
967
+ const finish = (fn) => {
968
+ if (done) {
969
+ return;
970
+ }
971
+ done = true;
972
+ if (timer) {
973
+ clearTimeout(timer);
974
+ }
975
+ ws.removeEventListener("open", onOpen);
976
+ ws.removeEventListener("error", onError);
977
+ abortSignal.removeEventListener("abort", onAbort);
978
+ fn();
979
+ };
980
+ timer = setTimeout(() => {
981
+ finish(() => {
982
+ closeWs();
983
+ reject(new Error("clawpool websocket connect timeout"));
984
+ });
985
+ }, timeoutMs);
986
+ ws.addEventListener("open", onOpen);
987
+ ws.addEventListener("error", onError);
988
+ abortSignal.addEventListener("abort", onAbort, { once: true });
989
+ });
990
+ }
991
+ async waitForCloseOrAbort(ws, abortSignal) {
992
+ return new Promise((resolve) => {
993
+ let settled = false;
994
+ const closeWs = () => {
995
+ this.safeCloseSpecificWs(ws);
996
+ };
997
+ function finish(result) {
998
+ if (settled) {
999
+ return;
1000
+ }
1001
+ settled = true;
1002
+ ws.removeEventListener("close", onClose);
1003
+ ws.removeEventListener("error", onError);
1004
+ abortSignal.removeEventListener("abort", onAbort);
1005
+ resolve(result);
1006
+ }
1007
+ function onClose(event) {
1008
+ const close = event;
1009
+ const code = Number(close.code);
1010
+ finish({
1011
+ cause: "close",
1012
+ aborted: false,
1013
+ closeCode: Number.isFinite(code) ? code : void 0,
1014
+ closeReason: normalizeCloseReason(close.reason)
1015
+ });
1016
+ }
1017
+ function onError() {
1018
+ finish({
1019
+ cause: "error",
1020
+ aborted: false
1021
+ });
1022
+ }
1023
+ function onAbort() {
1024
+ closeWs();
1025
+ finish({
1026
+ cause: "abort",
1027
+ aborted: true
1028
+ });
1029
+ }
1030
+ ws.addEventListener("close", onClose);
1031
+ ws.addEventListener("error", onError);
1032
+ abortSignal.addEventListener("abort", onAbort, { once: true });
1033
+ });
1034
+ }
1035
+ async authenticate(connSerial) {
1036
+ this.logInfo(`auth begin conn=${connSerial}`);
1037
+ const packet = await this.request(
1038
+ "auth",
1039
+ buildAuthPayload(this.account),
1040
+ {
1041
+ expected: ["auth_ack"],
1042
+ timeoutMs: 1e4,
1043
+ requireAuthed: false
1044
+ }
1045
+ );
1046
+ const payload = packet.payload ?? {};
1047
+ const code = parseCode(payload);
1048
+ if (code !== 0) {
1049
+ throw new AuthRejectedError(code, parseMessage(payload));
1050
+ }
1051
+ const heartbeatSec = parseHeartbeatSec(payload);
1052
+ const protocol = String(payload.protocol ?? "").trim() || void 0;
1053
+ this.updateStatus({ authed: true, lastError: null });
1054
+ this.logInfo(
1055
+ `auth success conn=${connSerial} heartbeatSec=${heartbeatSec} protocol=${protocol ?? "-"}`
1056
+ );
1057
+ return {
1058
+ heartbeatSec,
1059
+ protocol
1060
+ };
1061
+ }
1062
+ async handleMessageEvent(data, connSerial) {
1063
+ const text = await wsDataToText(data);
1064
+ if (!text) {
1065
+ return;
1066
+ }
1067
+ const resolvedConnSerial = (connSerial ?? this.activeConnectionSerial) || 0;
1068
+ let packet;
1069
+ try {
1070
+ packet = JSON.parse(text);
1071
+ } catch {
1072
+ this.logWarn(
1073
+ `ignored non-json message conn=${resolvedConnSerial} bytes=${text.length} preview=${JSON.stringify(text.slice(0, 200))}`
1074
+ );
1075
+ return;
1076
+ }
1077
+ const cmd = String(packet.cmd ?? "").trim();
1078
+ const seq = Number(packet.seq ?? 0);
1079
+ if (cmd !== "ping") {
1080
+ this.logInfo(
1081
+ `inbound packet conn=${resolvedConnSerial} cmd=${cmd || "-"} seq=${seq} bytes=${text.length}`
1082
+ );
1083
+ }
1084
+ if (cmd === "event_stop") {
1085
+ const payload = packet.payload;
1086
+ this.logInfo(
1087
+ `received stop-related packet cmd=${cmd} eventId=${String(payload.event_id ?? "").trim() || "-"} sessionId=${String(payload.session_id ?? "").trim() || "-"} stopId=${String(payload.stop_id ?? "").trim() || "-"} seq=${seq} bytes=${text.length}`
1088
+ );
1089
+ }
1090
+ if (cmd === "ping") {
1091
+ this.sendPacket("pong", { ts: Date.now() }, seq > 0 ? seq : void 0, false);
1092
+ return;
1093
+ }
1094
+ if (cmd === "event_msg") {
1095
+ this.callbacks.onEventMsg?.(packet.payload);
1096
+ return;
1097
+ }
1098
+ if (cmd === "event_react") {
1099
+ this.callbacks.onEventReact?.(packet.payload);
1100
+ return;
1101
+ }
1102
+ if (cmd === "event_revoke") {
1103
+ this.callbacks.onEventRevoke?.(packet.payload);
1104
+ return;
1105
+ }
1106
+ if (cmd === "event_stop") {
1107
+ const payload = packet.payload;
1108
+ this.logInfo(
1109
+ `received event_stop eventId=${String(payload.event_id ?? "").trim() || "-"} sessionId=${String(payload.session_id ?? "").trim() || "-"} stopId=${String(payload.stop_id ?? "").trim() || "-"} seq=${seq}`
1110
+ );
1111
+ this.callbacks.onEventStop?.(packet.payload);
1112
+ return;
1113
+ }
1114
+ if (cmd === "kicked") {
1115
+ const payload = packet.payload ?? {};
1116
+ const reason = parseKickedReason(payload);
1117
+ if (reason === "replaced_by_new_connection") {
1118
+ this.reconnectPenaltyAttemptFloor = Math.max(
1119
+ this.reconnectPenaltyAttemptFloor,
1120
+ this.reconnectPolicy.fastRetryDelaysMs.length + 5
1121
+ );
1122
+ this.logWarn(
1123
+ `apply reconnect penalty for kicked replacement penaltyFloor=${this.reconnectPenaltyAttemptFloor}`
1124
+ );
1125
+ }
1126
+ this.logWarn(`connection kicked by server reason=${reason}`);
1127
+ this.safeCloseWs("kicked_by_server");
1128
+ return;
1129
+ }
1130
+ const pending = this.pending.get(seq);
1131
+ if (pending && pending.expected.has(cmd)) {
1132
+ this.pending.delete(seq);
1133
+ clearTimeout(pending.timer);
1134
+ pending.resolve(packet);
1135
+ return;
1136
+ }
1137
+ }
1138
+ ensureReady(requireAuthed = true) {
1139
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1140
+ throw new Error("clawpool websocket is not open");
1141
+ }
1142
+ if (requireAuthed && !this.status.authed) {
1143
+ throw new Error("clawpool websocket is not authed");
1144
+ }
1145
+ }
1146
+ nextSeq() {
1147
+ this.seq += 1;
1148
+ return this.seq;
1149
+ }
1150
+ sendPacket(cmd, payload, seq, requireAuthed = true) {
1151
+ this.ensureReady(requireAuthed);
1152
+ const outSeq = seq ?? this.nextSeq();
1153
+ const packet = {
1154
+ cmd,
1155
+ seq: outSeq,
1156
+ payload
1157
+ };
1158
+ if (cmd === "event_stop_ack" || cmd === "event_stop_result") {
1159
+ this.logInfo(
1160
+ `send stop-related packet cmd=${cmd} eventId=${String(payload.event_id ?? "").trim() || "-"} stopId=${String(payload.stop_id ?? "").trim() || "-"} status=${String(payload.status ?? "").trim() || "-"} accepted=${String(payload.accepted ?? "").trim() || "-"} seq=${outSeq}`
1161
+ );
1162
+ }
1163
+ this.ws?.send(JSON.stringify(packet));
1164
+ return outSeq;
1165
+ }
1166
+ async request(cmd, payload, opts) {
1167
+ this.ensureReady(opts.requireAuthed ?? true);
1168
+ const seq = this.nextSeq();
1169
+ const expected = new Set(opts.expected);
1170
+ return new Promise((resolve, reject) => {
1171
+ const timer = setTimeout(() => {
1172
+ this.pending.delete(seq);
1173
+ reject(new Error(`${cmd} timeout`));
1174
+ }, opts.timeoutMs);
1175
+ this.pending.set(seq, {
1176
+ expected,
1177
+ resolve,
1178
+ reject,
1179
+ timer
1180
+ });
1181
+ try {
1182
+ const packet = {
1183
+ cmd,
1184
+ seq,
1185
+ payload
1186
+ };
1187
+ this.ws?.send(JSON.stringify(packet));
1188
+ } catch (err) {
1189
+ this.pending.delete(seq);
1190
+ clearTimeout(timer);
1191
+ reject(err instanceof Error ? err : new Error(String(err)));
1192
+ }
1193
+ });
1194
+ }
1195
+ rejectAllPending(err) {
1196
+ const pendingCount = this.pending.size;
1197
+ if (pendingCount > 0) {
1198
+ this.logWarn(`reject pending requests count=${pendingCount} reason=${err.message}`);
1199
+ }
1200
+ for (const [seq, pending] of this.pending.entries()) {
1201
+ this.pending.delete(seq);
1202
+ clearTimeout(pending.timer);
1203
+ pending.reject(err);
1204
+ }
1205
+ }
1206
+ buildSendTextPayload(sessionId, text, clientMsgId, opts) {
1207
+ const payload = {
1208
+ session_id: sessionId,
1209
+ client_msg_id: clientMsgId,
1210
+ msg_type: 1,
1211
+ content: text
1212
+ };
1213
+ const eventId = String(opts.eventId ?? "").trim();
1214
+ if (eventId) {
1215
+ payload.event_id = eventId;
1216
+ }
1217
+ if (opts.quotedMessageId) {
1218
+ payload.quoted_message_id = opts.quotedMessageId;
1219
+ }
1220
+ if (opts.extra && Object.keys(opts.extra).length > 0) {
1221
+ payload.extra = opts.extra;
1222
+ }
1223
+ return payload;
1224
+ }
1225
+ buildSendMediaPayload(sessionId, mediaUrl, caption, clientMsgId, opts) {
1226
+ const payload = {
1227
+ session_id: sessionId,
1228
+ client_msg_id: clientMsgId,
1229
+ msg_type: opts.msgType ?? 2,
1230
+ content: caption || "[media]",
1231
+ media_url: mediaUrl
1232
+ };
1233
+ const eventId = String(opts.eventId ?? "").trim();
1234
+ if (eventId) {
1235
+ payload.event_id = eventId;
1236
+ }
1237
+ if (opts.quotedMessageId) {
1238
+ payload.quoted_message_id = opts.quotedMessageId;
1239
+ }
1240
+ if (opts.extra && Object.keys(opts.extra).length > 0) {
1241
+ payload.extra = opts.extra;
1242
+ }
1243
+ return payload;
1244
+ }
1245
+ async sendMessageWithRetry(sessionId, payload, timeoutMs, action) {
1246
+ const maxAttempts = resolveAibotSendRetryMaxAttempts();
1247
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1248
+ await this.awaitSendMsgSlot(sessionId);
1249
+ const packet = await this.request("send_msg", payload, {
1250
+ expected: ["send_ack", "send_nack", "error"],
1251
+ timeoutMs
1252
+ });
1253
+ if (packet.cmd === "send_ack") {
1254
+ return packet.payload;
1255
+ }
1256
+ const err = this.packetError(packet);
1257
+ if (this.isRetryableSendError(err) && attempt < maxAttempts) {
1258
+ const delayMs = resolveAibotSendRetryDelayMs(attempt);
1259
+ this.logWarn(
1260
+ `${action} rate limited sessionId=${sessionId} attempt=${attempt}/${maxAttempts} delayMs=${delayMs}`
1261
+ );
1262
+ await sleep(delayMs);
1263
+ continue;
1264
+ }
1265
+ throw err;
1266
+ }
1267
+ throw new Error(`clawpool ${action} exhausted retry attempts`);
1268
+ }
1269
+ async sendSplitTextAfterSizeError(sessionId, text, clientMsgId, opts) {
1270
+ const chunks = splitTextForAibotProtocol(text, DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT);
1271
+ if (chunks.length <= 1) {
1272
+ throw new Error(`clawpool sendText size recovery failed clientMsgId=${clientMsgId}`);
1273
+ }
1274
+ this.logWarn(
1275
+ `sendText size recovery sessionId=${sessionId} clientMsgId=${clientMsgId} chunkCount=${chunks.length}`
1276
+ );
1277
+ let lastAck = null;
1278
+ for (let index = 0; index < chunks.length; index++) {
1279
+ const chunkClientMsgId = this.buildChunkClientMsgId(clientMsgId, index + 1);
1280
+ lastAck = await this.sendMessageWithRetry(
1281
+ sessionId,
1282
+ this.buildSendTextPayload(sessionId, chunks[index] ?? "", chunkClientMsgId, opts),
1283
+ opts.timeoutMs ?? 2e4,
1284
+ "sendText"
1285
+ );
1286
+ }
1287
+ if (lastAck == null) {
1288
+ throw new Error(`clawpool sendText size recovery produced no outbound chunks clientMsgId=${clientMsgId}`);
1289
+ }
1290
+ return lastAck;
1291
+ }
1292
+ async sendMediaCaptionAfterSizeError(sessionId, mediaUrl, caption, clientMsgId, opts) {
1293
+ const chunks = splitTextForAibotProtocol(caption, DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT);
1294
+ if (chunks.length <= 1) {
1295
+ throw new Error(`clawpool sendMedia size recovery failed clientMsgId=${clientMsgId}`);
1296
+ }
1297
+ this.logWarn(
1298
+ `sendMedia size recovery sessionId=${sessionId} clientMsgId=${clientMsgId} chunkCount=${chunks.length}`
1299
+ );
1300
+ const mediaAck = await this.sendMessageWithRetry(
1301
+ sessionId,
1302
+ this.buildSendMediaPayload(
1303
+ sessionId,
1304
+ mediaUrl,
1305
+ chunks[0] ?? "",
1306
+ `${clientMsgId}_media`,
1307
+ opts
1308
+ ),
1309
+ opts.timeoutMs ?? 3e4,
1310
+ "sendMedia"
1311
+ );
1312
+ for (let index = 1; index < chunks.length; index++) {
1313
+ const chunkClientMsgId = this.buildChunkClientMsgId(clientMsgId, index);
1314
+ await this.sendMessageWithRetry(
1315
+ sessionId,
1316
+ this.buildSendTextPayload(sessionId, chunks[index] ?? "", chunkClientMsgId, opts),
1317
+ opts.timeoutMs ?? 2e4,
1318
+ "sendText"
1319
+ );
1320
+ }
1321
+ return mediaAck;
1322
+ }
1323
+ async awaitSendMsgSlot(sessionId) {
1324
+ const normalizedSessionId = String(sessionId ?? "").trim();
1325
+ if (!normalizedSessionId) {
1326
+ return;
1327
+ }
1328
+ for (; ; ) {
1329
+ const now = Date.now();
1330
+ const recent = pruneAibotSendWindow(this.sendMsgWindowBySession.get(normalizedSessionId) ?? [], now);
1331
+ const waitMs = computeAibotSendThrottleDelayMs(recent, now);
1332
+ this.sendMsgWindowBySession.set(normalizedSessionId, recent);
1333
+ if (waitMs <= 0) {
1334
+ recent.push(now);
1335
+ this.sendMsgWindowBySession.set(normalizedSessionId, recent);
1336
+ return;
1337
+ }
1338
+ this.logWarn(
1339
+ `send_msg pacing sessionId=${normalizedSessionId} queued=${recent.length} waitMs=${waitMs}`
1340
+ );
1341
+ await sleep(waitMs);
1342
+ }
1343
+ }
1344
+ isRetryableSendError(err) {
1345
+ return err instanceof AibotPacketError && isRetryableAibotSendCode(err.code);
1346
+ }
1347
+ isMessageTooLargeError(err) {
1348
+ return err instanceof AibotPacketError && err.code === 4004;
1349
+ }
1350
+ buildChunkClientMsgId(clientMsgId, chunkIndex) {
1351
+ return `${clientMsgId}_chunk${chunkIndex}`;
1352
+ }
1353
+ packetError(packet) {
1354
+ const payload = packet.payload;
1355
+ const code = Number(payload.code ?? 0);
1356
+ const msg = String(payload.msg ?? packet.cmd ?? "unknown error");
1357
+ return new AibotPacketError(packet.cmd, code, msg);
1358
+ }
1359
+ normalizeStreamDeltaContent(clientMsgId, deltaContent, isFinish) {
1360
+ const carry = this.pendingStreamHighSurrogate.get(clientMsgId) ?? "";
1361
+ this.pendingStreamHighSurrogate.delete(clientMsgId);
1362
+ let normalized = `${carry}${String(deltaContent ?? "")}`;
1363
+ if (!normalized) {
1364
+ return "";
1365
+ }
1366
+ if (isFinish && !deltaContent && carry) {
1367
+ this.logWarn(`dropping dangling high surrogate at stream finish clientMsgId=${clientMsgId}`);
1368
+ return "";
1369
+ }
1370
+ if (!isFinish && this.endsWithHighSurrogate(normalized)) {
1371
+ this.pendingStreamHighSurrogate.set(clientMsgId, normalized.slice(-1));
1372
+ normalized = normalized.slice(0, -1);
1373
+ } else if (isFinish && this.endsWithHighSurrogate(normalized)) {
1374
+ this.logWarn(`dropping dangling high surrogate at stream finish clientMsgId=${clientMsgId}`);
1375
+ normalized = normalized.slice(0, -1);
1376
+ }
1377
+ return normalized;
1378
+ }
1379
+ endsWithHighSurrogate(value) {
1380
+ if (!value) {
1381
+ return false;
1382
+ }
1383
+ const code = value.charCodeAt(value.length - 1);
1384
+ return code >= 55296 && code <= 56319;
1385
+ }
1386
+ nextConnectionSerial() {
1387
+ this.connectionSerial += 1;
1388
+ return this.connectionSerial;
1389
+ }
1390
+ resolveKeepalivePolicy(heartbeatSec) {
1391
+ const defaultIntervalMs = Math.max(5e3, Math.min(2e4, Math.floor(heartbeatSec * 1e3 / 2)));
1392
+ const intervalMs = clampInt(
1393
+ this.account.config.keepalivePingMs,
1394
+ defaultIntervalMs,
1395
+ 2e3,
1396
+ 6e4
1397
+ );
1398
+ const defaultTimeoutMs = Math.max(3e3, Math.min(15e3, Math.floor(intervalMs * 0.8)));
1399
+ const timeoutMs = clampInt(
1400
+ this.account.config.keepaliveTimeoutMs,
1401
+ defaultTimeoutMs,
1402
+ 1e3,
1403
+ 6e4
1404
+ );
1405
+ return {
1406
+ intervalMs,
1407
+ timeoutMs
1408
+ };
1409
+ }
1410
+ startKeepalive(ws, connSerial, heartbeatSec) {
1411
+ this.stopKeepalive();
1412
+ const policy = this.resolveKeepalivePolicy(heartbeatSec);
1413
+ this.logInfo(
1414
+ `keepalive start conn=${connSerial} intervalMs=${policy.intervalMs} timeoutMs=${policy.timeoutMs} serverHeartbeatSec=${heartbeatSec}`
1415
+ );
1416
+ this.keepaliveTimer = setInterval(() => {
1417
+ void this.runKeepaliveProbe(ws, connSerial, policy.timeoutMs);
1418
+ }, policy.intervalMs);
1419
+ }
1420
+ stopKeepalive() {
1421
+ if (this.keepaliveTimer) {
1422
+ clearInterval(this.keepaliveTimer);
1423
+ this.keepaliveTimer = null;
1424
+ }
1425
+ this.keepaliveInFlight = false;
1426
+ }
1427
+ async runKeepaliveProbe(ws, connSerial, timeoutMs) {
1428
+ if (!this.running || this.ws !== ws || ws.readyState !== WebSocket.OPEN || !this.status.authed) {
1429
+ return;
1430
+ }
1431
+ if (this.keepaliveInFlight) {
1432
+ this.logWarn(`keepalive overlap detected conn=${connSerial}, force reconnect`);
1433
+ this.safeCloseSpecificWs(ws, "keepalive_overlap");
1434
+ return;
1435
+ }
1436
+ this.keepaliveInFlight = true;
1437
+ const startedAt = Date.now();
1438
+ try {
1439
+ await this.request(
1440
+ "ping",
1441
+ {
1442
+ ts: startedAt,
1443
+ source: "clawpool_keepalive"
1444
+ },
1445
+ {
1446
+ expected: ["pong"],
1447
+ timeoutMs
1448
+ }
1449
+ );
1450
+ const latencyMs = Math.max(0, Date.now() - startedAt);
1451
+ if (latencyMs >= 2e3) {
1452
+ this.logWarn(`keepalive high latency conn=${connSerial} latencyMs=${latencyMs}`);
1453
+ }
1454
+ } catch (err) {
1455
+ const msg = err instanceof Error ? err.message : String(err);
1456
+ this.logWarn(`keepalive failed conn=${connSerial} err=${msg}, force reconnect`);
1457
+ if (this.ws === ws) {
1458
+ this.safeCloseSpecificWs(ws, "keepalive_probe_failed");
1459
+ }
1460
+ } finally {
1461
+ this.keepaliveInFlight = false;
1462
+ }
1463
+ }
1464
+ resolveReconnectDelayMs(attempt) {
1465
+ if (attempt <= 0) {
1466
+ return 0;
1467
+ }
1468
+ const fastRetryDelays = this.reconnectPolicy.fastRetryDelaysMs;
1469
+ if (attempt <= fastRetryDelays.length) {
1470
+ return fastRetryDelays[attempt - 1] ?? this.reconnectPolicy.baseDelayMs;
1471
+ }
1472
+ const exponent = attempt - fastRetryDelays.length - 1;
1473
+ const uncapped = this.reconnectPolicy.baseDelayMs * 2 ** exponent;
1474
+ const capped = Math.min(this.reconnectPolicy.maxDelayMs, Math.floor(uncapped));
1475
+ const jitterFloor = Math.max(100, Math.floor(capped * 0.5));
1476
+ return randomIntInclusive(jitterFloor, capped);
1477
+ }
1478
+ consumeReconnectPenaltyAttemptFloor() {
1479
+ const floor = this.reconnectPenaltyAttemptFloor;
1480
+ this.reconnectPenaltyAttemptFloor = 0;
1481
+ return floor;
1482
+ }
1483
+ safeCloseSpecificWs(ws, reason = "") {
1484
+ try {
1485
+ ws.close();
1486
+ } catch {
1487
+ }
1488
+ if (this.ws === ws) {
1489
+ this.ws = null;
1490
+ }
1491
+ }
1492
+ safeCloseWs(reason = "") {
1493
+ if (!this.ws) {
1494
+ return;
1495
+ }
1496
+ this.safeCloseSpecificWs(this.ws, reason);
1497
+ }
1498
+ updateStatus(patch) {
1499
+ this.status = {
1500
+ ...this.status,
1501
+ ...patch
1502
+ };
1503
+ this.callbacks.onStatus?.(this.getStatus());
1504
+ }
1505
+ };
1506
+ var activeClients = /* @__PURE__ */ new Map();
1507
+ function setActiveAibotClient(accountId, client) {
1508
+ if (!accountId) {
1509
+ return;
1510
+ }
1511
+ if (!client) {
1512
+ activeClients.delete(accountId);
1513
+ return;
1514
+ }
1515
+ activeClients.set(accountId, client);
1516
+ }
1517
+ function clearActiveAibotClient(accountId, client) {
1518
+ if (!accountId) {
1519
+ return;
1520
+ }
1521
+ if (activeClients.get(accountId) !== client) {
1522
+ return;
1523
+ }
1524
+ activeClients.delete(accountId);
1525
+ }
1526
+ function getActiveAibotClient(accountId) {
1527
+ if (!accountId) {
1528
+ return null;
1529
+ }
1530
+ return activeClients.get(accountId) ?? null;
1531
+ }
1532
+ function requireActiveAibotClient(accountId) {
1533
+ const client = getActiveAibotClient(accountId);
1534
+ if (!client) {
1535
+ throw new Error(
1536
+ `clawpool account "${accountId}" is not connected; start the gateway channel runtime first`
1537
+ );
1538
+ }
1539
+ return client;
1540
+ }
1541
+
1542
+ // src/silent-unsend-completion.ts
1543
+ var COMPLETION_TTL_MS = 5 * 60 * 1e3;
1544
+ var completedAtByMessageId = /* @__PURE__ */ new Map();
1545
+ function normalizeMessageId(value) {
1546
+ const normalized = String(value ?? "").trim();
1547
+ if (!/^\d+$/.test(normalized)) {
1548
+ return "";
1549
+ }
1550
+ return normalized;
1551
+ }
1552
+ function pruneExpired(now) {
1553
+ for (const [messageId, completedAt] of completedAtByMessageId) {
1554
+ if (now - completedAt > COMPLETION_TTL_MS) {
1555
+ completedAtByMessageId.delete(messageId);
1556
+ }
1557
+ }
1558
+ }
1559
+ function markSilentUnsendCompleted(messageId) {
1560
+ const normalizedMessageId = normalizeMessageId(messageId);
1561
+ if (!normalizedMessageId) {
1562
+ return;
1563
+ }
1564
+ const now = Date.now();
1565
+ pruneExpired(now);
1566
+ completedAtByMessageId.set(normalizedMessageId, now);
1567
+ }
1568
+ function consumeSilentUnsendCompleted(messageId) {
1569
+ const normalizedMessageId = normalizeMessageId(messageId);
1570
+ if (!normalizedMessageId) {
1571
+ return false;
1572
+ }
1573
+ pruneExpired(Date.now());
1574
+ const hadCompletion = completedAtByMessageId.has(normalizedMessageId);
1575
+ if (hadCompletion) {
1576
+ completedAtByMessageId.delete(normalizedMessageId);
1577
+ }
1578
+ return hadCompletion;
1579
+ }
1580
+
1581
+ // src/target-resolver.ts
1582
+ var aibotSessionIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1583
+ function isAibotSessionID(value) {
1584
+ const normalized = String(value ?? "").trim();
1585
+ if (!normalized) {
1586
+ return false;
1587
+ }
1588
+ return aibotSessionIDPattern.test(normalized);
1589
+ }
1590
+ function normalizeAibotSessionTarget2(raw) {
1591
+ const trimmed = String(raw ?? "").trim();
1592
+ if (!trimmed) {
1593
+ return "";
1594
+ }
1595
+ return trimmed.replace(/^clawpool:/i, "").replace(/^session:/i, "").trim();
1596
+ }
1597
+ function buildRouteSessionKeyCandidates(rawTarget, normalizedTarget) {
1598
+ if (rawTarget === normalizedTarget) {
1599
+ return [rawTarget];
1600
+ }
1601
+ return [rawTarget, normalizedTarget].filter((candidate) => candidate.length > 0);
1602
+ }
1603
+ async function resolveAibotOutboundTarget(params) {
1604
+ const rawTarget = String(params.to ?? "").trim();
1605
+ if (!rawTarget) {
1606
+ throw new Error("clawpool outbound target must be non-empty");
1607
+ }
1608
+ const normalizedTarget = normalizeAibotSessionTarget2(rawTarget);
1609
+ if (!normalizedTarget) {
1610
+ throw new Error("clawpool outbound target must contain session_id or route_session_key");
1611
+ }
1612
+ if (isAibotSessionID(normalizedTarget)) {
1613
+ return {
1614
+ sessionId: normalizedTarget,
1615
+ rawTarget,
1616
+ normalizedTarget,
1617
+ resolveSource: "direct"
1618
+ };
1619
+ }
1620
+ if (/^\d+$/.test(normalizedTarget)) {
1621
+ throw new Error(
1622
+ `clawpool outbound target "${rawTarget}" is numeric; expected session_id(UUID) or route.sessionKey`
1623
+ );
1624
+ }
1625
+ const routeSessionKeyCandidates = buildRouteSessionKeyCandidates(rawTarget, normalizedTarget);
1626
+ let lastResolveError = null;
1627
+ for (const routeSessionKey of routeSessionKeyCandidates) {
1628
+ try {
1629
+ const ack = await params.client.resolveSessionRoute("clawpool", params.accountId, routeSessionKey);
1630
+ const sessionId = String(ack.session_id ?? "").trim();
1631
+ if (!isAibotSessionID(sessionId)) {
1632
+ throw new Error(
1633
+ `session_route_resolve returned invalid session_id for route_session_key="${routeSessionKey}"`
1634
+ );
1635
+ }
1636
+ return {
1637
+ sessionId,
1638
+ rawTarget,
1639
+ normalizedTarget,
1640
+ resolveSource: "sessionRouteMap"
1641
+ };
1642
+ } catch (err) {
1643
+ lastResolveError = err instanceof Error ? err : new Error(String(err));
1644
+ }
1645
+ }
1646
+ if (lastResolveError) {
1647
+ throw new Error(
1648
+ `clawpool outbound target resolve failed target="${rawTarget}" accountId=${params.accountId}: ${lastResolveError.message}`
1649
+ );
1650
+ }
1651
+ throw new Error(`clawpool outbound target resolve failed target="${rawTarget}" accountId=${params.accountId}`);
1652
+ }
1653
+
1654
+ // src/delete-target-resolver.ts
1655
+ async function resolveAibotDeleteTarget(params) {
1656
+ const rawTarget = String(params.sessionId ?? "").trim() || String(params.to ?? "").trim() || String(params.topic ?? "").trim() || String(params.currentChannelId ?? "").trim();
1657
+ if (!rawTarget) {
1658
+ return "";
1659
+ }
1660
+ const resolved = await resolveAibotOutboundTarget({
1661
+ client: params.client,
1662
+ accountId: params.accountId,
1663
+ to: rawTarget
1664
+ });
1665
+ return resolved.sessionId;
1666
+ }
1667
+
1668
+ // src/silent-unsend-plan.ts
1669
+ function normalizeMessageId2(value) {
1670
+ const normalized = String(value ?? "").trim();
1671
+ if (!/^\d+$/.test(normalized)) {
1672
+ return "";
1673
+ }
1674
+ return normalized;
1675
+ }
1676
+ async function resolveSilentUnsendPlan(params) {
1677
+ const targetMessageId = normalizeMessageId2(params.messageId);
1678
+ if (!targetMessageId) {
1679
+ throw new Error("Clawpool unsend requires numeric messageId.");
1680
+ }
1681
+ const targetSessionId = await resolveAibotDeleteTarget({
1682
+ client: params.client,
1683
+ accountId: params.accountId,
1684
+ sessionId: params.targetSessionId,
1685
+ to: params.targetTo,
1686
+ topic: params.targetTopic,
1687
+ currentChannelId: params.currentChannelId
1688
+ });
1689
+ if (!targetSessionId) {
1690
+ throw new Error(
1691
+ "Clawpool unsend requires sessionId or to, or must be used inside an active Clawpool conversation."
1692
+ );
1693
+ }
1694
+ const targetDelete = {
1695
+ sessionId: targetSessionId,
1696
+ messageId: targetMessageId
1697
+ };
1698
+ const currentMessageId = normalizeMessageId2(params.currentMessageId);
1699
+ if (!currentMessageId) {
1700
+ return { targetDelete };
1701
+ }
1702
+ if (currentMessageId === targetMessageId) {
1703
+ return {
1704
+ targetDelete,
1705
+ completionMessageId: currentMessageId
1706
+ };
1707
+ }
1708
+ const currentChannelId = String(params.currentChannelId ?? "").trim();
1709
+ if (!currentChannelId) {
1710
+ return { targetDelete };
1711
+ }
1712
+ const currentSessionId = await resolveAibotDeleteTarget({
1713
+ client: params.client,
1714
+ accountId: params.accountId,
1715
+ currentChannelId
1716
+ });
1717
+ if (!currentSessionId) {
1718
+ throw new Error("Clawpool unsend could not resolve the current command message session.");
1719
+ }
1720
+ return {
1721
+ targetDelete,
1722
+ commandDelete: {
1723
+ sessionId: currentSessionId,
1724
+ messageId: currentMessageId
1725
+ },
1726
+ completionMessageId: currentMessageId
1727
+ };
1728
+ }
1729
+
1730
+ // src/actions.ts
1731
+ var WS_ACTIONS = /* @__PURE__ */ new Set(["unsend", "delete"]);
1732
+ var DISCOVERABLE_ACTIONS = ["unsend", "delete"];
1733
+ function toSnakeCaseKey(key) {
1734
+ return key.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
1735
+ }
1736
+ function readStringishParam(params, key) {
1737
+ const value = readStringParam(params, key);
1738
+ if (value) {
1739
+ return value;
1740
+ }
1741
+ const snakeKey = toSnakeCaseKey(key);
1742
+ const raw = (Object.hasOwn(params, key) ? params[key] : void 0) ?? (snakeKey !== key && Object.hasOwn(params, snakeKey) ? params[snakeKey] : void 0);
1743
+ if (typeof raw === "number" && Number.isFinite(raw)) {
1744
+ return String(raw);
1745
+ }
1746
+ return void 0;
1747
+ }
1748
+ var aibotMessageActions = {
1749
+ listActions: ({ cfg }) => {
1750
+ const hasConfiguredAccount = listAibotAccountIds(cfg).map((accountId) => resolveAibotAccount({ cfg, accountId })).some((account) => account.enabled && account.configured);
1751
+ if (!hasConfiguredAccount) {
1752
+ return [];
1753
+ }
1754
+ return DISCOVERABLE_ACTIONS;
1755
+ },
1756
+ supportsAction: ({ action }) => {
1757
+ const normalizedAction = String(action ?? "").trim();
1758
+ return WS_ACTIONS.has(normalizedAction);
1759
+ },
1760
+ handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
1761
+ const normalizedAction = String(action ?? "").trim();
1762
+ if (!WS_ACTIONS.has(normalizedAction)) {
1763
+ throw new Error(`Clawpool action ${normalizedAction} is not supported`);
1764
+ }
1765
+ const account = resolveAibotAccount({ cfg, accountId });
1766
+ if (!account.enabled) {
1767
+ throw new Error(`Clawpool account "${account.accountId}" is disabled.`);
1768
+ }
1769
+ if (!account.configured) {
1770
+ throw new Error(`Clawpool account "${account.accountId}" is not configured.`);
1771
+ }
1772
+ const client = requireActiveAibotClient(account.accountId);
1773
+ const messageId = readStringishParam(params, "messageId") ?? readStringishParam(params, "msgId");
1774
+ if (!messageId) {
1775
+ throw new Error("Clawpool unsend requires messageId.");
1776
+ }
1777
+ const plan = await resolveSilentUnsendPlan({
1778
+ client,
1779
+ accountId: account.accountId,
1780
+ messageId,
1781
+ targetSessionId: readStringishParam(params, "sessionId"),
1782
+ targetTo: readStringishParam(params, "to"),
1783
+ targetTopic: readStringishParam(params, "topic"),
1784
+ currentChannelId: toolContext?.currentChannelId,
1785
+ currentMessageId: toolContext?.currentMessageId
1786
+ });
1787
+ const ack = await client.deleteMessage(plan.targetDelete.sessionId, plan.targetDelete.messageId);
1788
+ if (plan.commandDelete) {
1789
+ await client.deleteMessage(plan.commandDelete.sessionId, plan.commandDelete.messageId);
1790
+ }
1791
+ if (plan.completionMessageId) {
1792
+ markSilentUnsendCompleted(plan.completionMessageId);
1793
+ }
1794
+ return jsonResult({
1795
+ ok: true,
1796
+ deleted: true,
1797
+ unsent: normalizedAction === "unsend",
1798
+ messageId: String(ack.msg_id ?? messageId),
1799
+ sessionId: String(ack.session_id ?? plan.targetDelete.sessionId)
1800
+ });
1801
+ }
1802
+ };
1803
+
1804
+ // src/exec-approval-adapter-payload.ts
1805
+ var DEFAULT_ALLOWED_DECISIONS = ["allow-once", "allow-always", "deny"];
1806
+ function normalizeText(value) {
1807
+ return String(value ?? "").replace(/\r\n/g, "\n").trim();
1808
+ }
1809
+ function stripUndefinedFields(record) {
1810
+ const next = {};
1811
+ for (const [key, value] of Object.entries(record)) {
1812
+ if (value !== void 0) {
1813
+ next[key] = value;
1814
+ }
1815
+ }
1816
+ return next;
1817
+ }
1818
+ function buildFence(text, language) {
1819
+ let fence = "```";
1820
+ while (text.includes(fence)) {
1821
+ fence += "`";
1822
+ }
1823
+ return `${fence}${language ?? ""}
1824
+ ${text}
1825
+ ${fence}`;
1826
+ }
1827
+ function resolveHost(value) {
1828
+ return normalizeText(value) === "node" ? "node" : "gateway";
1829
+ }
1830
+ function resolveApprovalSlug(approvalId) {
1831
+ return approvalId.length <= 8 ? approvalId : approvalId.slice(0, 8);
1832
+ }
1833
+ function resolveCommandText(params) {
1834
+ return normalizeText(params.commandPreview) || normalizeText(params.command);
1835
+ }
1836
+ function buildPendingApprovalText(params) {
1837
+ const lines = [];
1838
+ const warningText = params.warningText?.trim();
1839
+ if (warningText) {
1840
+ lines.push(warningText);
1841
+ }
1842
+ lines.push("Approval required.");
1843
+ lines.push("Run:");
1844
+ lines.push(buildFence(`/approve ${params.approvalCommandId} allow-once`, "txt"));
1845
+ lines.push("Pending command:");
1846
+ lines.push(buildFence(params.command, "sh"));
1847
+ lines.push("Other options:");
1848
+ lines.push(
1849
+ buildFence(
1850
+ `/approve ${params.approvalCommandId} allow-always
1851
+ /approve ${params.approvalCommandId} deny`,
1852
+ "txt"
1853
+ )
1854
+ );
1855
+ const info = [];
1856
+ info.push(`Host: ${params.host}`);
1857
+ if (params.nodeId) {
1858
+ info.push(`Node: ${params.nodeId}`);
1859
+ }
1860
+ if (params.cwd) {
1861
+ info.push(`CWD: ${params.cwd}`);
1862
+ }
1863
+ if (params.expiresInSeconds !== void 0) {
1864
+ info.push(`Expires in: ${params.expiresInSeconds}s`);
1865
+ }
1866
+ info.push(`Full id: \`${params.approvalId}\``);
1867
+ lines.push(info.join("\n"));
1868
+ return lines.join("\n\n");
1869
+ }
1870
+ function decisionLabel(decision) {
1871
+ if (decision === "allow-once") {
1872
+ return "allowed once";
1873
+ }
1874
+ if (decision === "allow-always") {
1875
+ return "allowed always";
1876
+ }
1877
+ return "denied";
1878
+ }
1879
+ function mapResolvedStatus(decision) {
1880
+ if (decision === "allow-once") {
1881
+ return "resolved-allow-once";
1882
+ }
1883
+ if (decision === "allow-always") {
1884
+ return "resolved-allow-always";
1885
+ }
1886
+ return "resolved-deny";
1887
+ }
1888
+ function buildResolvedApprovalText(params) {
1889
+ const by = params.resolvedBy ? ` Resolved by ${params.resolvedBy}.` : "";
1890
+ return `\u2705 Exec approval ${decisionLabel(params.decision)}.${by} ID: ${params.approvalId}`;
1891
+ }
1892
+ function buildClawpoolPendingExecApprovalPayload(params) {
1893
+ const approvalId = normalizeText(params.request.id);
1894
+ const command = resolveCommandText(params.request.request);
1895
+ if (!approvalId || !command) {
1896
+ return null;
1897
+ }
1898
+ const approvalSlug = resolveApprovalSlug(approvalId);
1899
+ const approvalCommandId = approvalId;
1900
+ const host = resolveHost(params.request.request.host);
1901
+ const nodeId = normalizeText(params.request.request.nodeId) || void 0;
1902
+ const cwd = normalizeText(params.request.request.cwd) || void 0;
1903
+ const expiresInSeconds = Math.max(
1904
+ 0,
1905
+ Math.round((params.request.expiresAtMs - params.nowMs) / 1e3)
1906
+ );
1907
+ return {
1908
+ text: buildPendingApprovalText({
1909
+ approvalId,
1910
+ approvalCommandId,
1911
+ command,
1912
+ host,
1913
+ nodeId,
1914
+ cwd,
1915
+ expiresInSeconds
1916
+ }),
1917
+ channelData: {
1918
+ execApproval: {
1919
+ approvalId,
1920
+ approvalSlug,
1921
+ allowedDecisions: [...DEFAULT_ALLOWED_DECISIONS]
1922
+ },
1923
+ clawpool: {
1924
+ execApproval: stripUndefinedFields({
1925
+ approval_command_id: approvalCommandId,
1926
+ command,
1927
+ host,
1928
+ node_id: nodeId,
1929
+ cwd,
1930
+ expires_in_seconds: expiresInSeconds
1931
+ })
1932
+ }
1933
+ }
1934
+ };
1935
+ }
1936
+ function buildClawpoolResolvedExecApprovalPayload(params) {
1937
+ const approvalId = normalizeText(params.resolved.id);
1938
+ if (!approvalId) {
1939
+ return null;
1940
+ }
1941
+ const decision = params.resolved.decision;
1942
+ const resolvedBy = normalizeText(params.resolved.resolvedBy) || void 0;
1943
+ const host = params.resolved.request?.host ? resolveHost(params.resolved.request.host) : void 0;
1944
+ const nodeId = normalizeText(params.resolved.request?.nodeId) || void 0;
1945
+ const approvalCommandId = approvalId;
1946
+ const summary = `Exec approval ${decisionLabel(decision)}.`;
1947
+ const detailText = resolvedBy ? `Resolved by ${resolvedBy}.` : void 0;
1948
+ const structuredStatus = stripUndefinedFields({
1949
+ status: mapResolvedStatus(decision),
1950
+ summary,
1951
+ detail_text: detailText,
1952
+ approval_id: approvalId,
1953
+ approval_command_id: approvalCommandId,
1954
+ host,
1955
+ node_id: nodeId,
1956
+ decision,
1957
+ resolved_by_id: resolvedBy
1958
+ });
1959
+ return {
1960
+ text: buildResolvedApprovalText({
1961
+ approvalId,
1962
+ decision,
1963
+ resolvedBy
1964
+ }),
1965
+ channelData: {
1966
+ clawpool: {
1967
+ execStatus: structuredStatus
1968
+ }
1969
+ }
1970
+ };
1971
+ }
1972
+
1973
+ // src/channel-exec-approvals.ts
1974
+ function hasConfiguredApprovers(values) {
1975
+ return values?.some((value) => {
1976
+ const normalized = String(value ?? "").trim();
1977
+ return normalized.length > 0;
1978
+ }) ?? false;
1979
+ }
1980
+ function isClawpoolExecApprovalClientEnabled(params) {
1981
+ const account = resolveAibotAccount(params);
1982
+ return Boolean(
1983
+ account.config.execApprovals?.enabled && hasConfiguredApprovers(account.config.execApprovals.approvers)
1984
+ );
1985
+ }
1986
+ var clawpoolExecApprovalAdapter = {
1987
+ getInitiatingSurfaceState: ({ cfg, accountId }) => isClawpoolExecApprovalClientEnabled({ cfg, accountId }) ? { kind: "enabled" } : { kind: "disabled" },
1988
+ shouldSuppressLocalPrompt: ({ cfg, accountId, payload }) => {
1989
+ if (!isClawpoolExecApprovalClientEnabled({ cfg, accountId })) {
1990
+ return false;
1991
+ }
1992
+ const channelData = payload.channelData;
1993
+ if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
1994
+ return false;
1995
+ }
1996
+ const execApproval = channelData.execApproval;
1997
+ return Boolean(execApproval) && typeof execApproval === "object" && !Array.isArray(execApproval);
1998
+ },
1999
+ hasConfiguredDmRoute: () => false,
2000
+ shouldSuppressForwardingFallback: () => false,
2001
+ buildPendingPayload: (params) => buildClawpoolPendingExecApprovalPayload(params),
2002
+ buildResolvedPayload: (params) => buildClawpoolResolvedExecApprovalPayload(params),
2003
+ beforeDeliverPending: () => void 0
2004
+ };
2005
+
2006
+ // src/monitor.ts
2007
+ import {
2008
+ createReplyPrefixOptions
2009
+ } from "openclaw/plugin-sdk/channel-runtime";
2010
+
2011
+ // src/active-reply-runs.ts
2012
+ var runsByEvent = /* @__PURE__ */ new Map();
2013
+ var eventKeyBySession = /* @__PURE__ */ new Map();
2014
+ function buildEventKey(accountId, eventId) {
2015
+ return `${String(accountId ?? "").trim()}:${String(eventId ?? "").trim()}`;
2016
+ }
2017
+ function buildSessionKey(accountId, sessionId) {
2018
+ return `${String(accountId ?? "").trim()}:${String(sessionId ?? "").trim()}`;
2019
+ }
2020
+ function registerActiveReplyRun(params) {
2021
+ const accountId = String(params.accountId ?? "").trim();
2022
+ const eventId = String(params.eventId ?? "").trim();
2023
+ const sessionId = String(params.sessionId ?? "").trim();
2024
+ if (!accountId || !eventId || !sessionId) {
2025
+ return null;
2026
+ }
2027
+ const eventKey = buildEventKey(accountId, eventId);
2028
+ const sessionKey = buildSessionKey(accountId, sessionId);
2029
+ const existingEventKey = eventKeyBySession.get(sessionKey);
2030
+ if (existingEventKey && existingEventKey !== eventKey) {
2031
+ const existing = runsByEvent.get(existingEventKey);
2032
+ if (existing) {
2033
+ existing.abortReason = existing.abortReason || "superseded_by_new_event";
2034
+ existing.controller.abort(existing.abortReason);
2035
+ runsByEvent.delete(existingEventKey);
2036
+ }
2037
+ }
2038
+ const run = {
2039
+ accountId,
2040
+ eventId,
2041
+ sessionId,
2042
+ controller: params.controller,
2043
+ stopRequested: false
2044
+ };
2045
+ runsByEvent.set(eventKey, run);
2046
+ eventKeyBySession.set(sessionKey, eventKey);
2047
+ return run;
2048
+ }
2049
+ function resolveActiveReplyRun(params) {
2050
+ const accountId = String(params.accountId ?? "").trim();
2051
+ if (!accountId) {
2052
+ return null;
2053
+ }
2054
+ const eventId = String(params.eventId ?? "").trim();
2055
+ if (eventId) {
2056
+ return runsByEvent.get(buildEventKey(accountId, eventId)) ?? null;
2057
+ }
2058
+ const sessionId = String(params.sessionId ?? "").trim();
2059
+ if (!sessionId) {
2060
+ return null;
2061
+ }
2062
+ const eventKey = eventKeyBySession.get(buildSessionKey(accountId, sessionId));
2063
+ if (!eventKey) {
2064
+ return null;
2065
+ }
2066
+ return runsByEvent.get(eventKey) ?? null;
2067
+ }
2068
+ function clearActiveReplyRun(run) {
2069
+ if (!run) {
2070
+ return;
2071
+ }
2072
+ const eventKey = buildEventKey(run.accountId, run.eventId);
2073
+ const sessionKey = buildSessionKey(run.accountId, run.sessionId);
2074
+ const current = runsByEvent.get(eventKey);
2075
+ if (current === run) {
2076
+ runsByEvent.delete(eventKey);
2077
+ }
2078
+ if (eventKeyBySession.get(sessionKey) === eventKey) {
2079
+ eventKeyBySession.delete(sessionKey);
2080
+ }
2081
+ }
2082
+
2083
+ // src/reply-text-guard.ts
2084
+ var NETWORK_ERROR_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
2085
+ var TIMEOUT_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
2086
+ var CONTEXT_OVERFLOW_MESSAGE = "\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u8FC7\u957F\uFF0C\u8BF7\u65B0\u5F00\u4F1A\u8BDD\u540E\u91CD\u8BD5\u3002";
2087
+ var GENERIC_STOP_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u5F02\u5E38\u4E2D\u65AD\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
2088
+ function guardInternalReplyText(rawText) {
2089
+ const normalized = String(rawText ?? "").trim();
2090
+ if (!normalized) {
2091
+ return null;
2092
+ }
2093
+ if (/^Unhandled stop reason:\s*network_error$/i.test(normalized)) {
2094
+ return {
2095
+ code: "upstream_network_error",
2096
+ rawText: normalized,
2097
+ userText: NETWORK_ERROR_MESSAGE
2098
+ };
2099
+ }
2100
+ if (/^LLM request timed out\.?$/i.test(normalized)) {
2101
+ return {
2102
+ code: "upstream_timeout",
2103
+ rawText: normalized,
2104
+ userText: TIMEOUT_MESSAGE
2105
+ };
2106
+ }
2107
+ if (normalized.startsWith("Context overflow: prompt too large for the model.")) {
2108
+ return {
2109
+ code: "upstream_context_overflow",
2110
+ rawText: normalized,
2111
+ userText: CONTEXT_OVERFLOW_MESSAGE
2112
+ };
2113
+ }
2114
+ if (/^Unhandled stop reason:\s*[a-z0-9_]+$/i.test(normalized)) {
2115
+ return {
2116
+ code: "upstream_stop_reason",
2117
+ rawText: normalized,
2118
+ userText: GENERIC_STOP_MESSAGE
2119
+ };
2120
+ }
2121
+ return null;
2122
+ }
2123
+
2124
+ // src/upstream-retry.ts
2125
+ var DEFAULT_UPSTREAM_RETRY_MAX_ATTEMPTS = 3;
2126
+ var DEFAULT_UPSTREAM_RETRY_BASE_DELAY_MS = 300;
2127
+ var DEFAULT_UPSTREAM_RETRY_MAX_DELAY_MS = 2e3;
2128
+ function clampInt2(value, fallback, min, max) {
2129
+ const n = Number(value);
2130
+ if (!Number.isFinite(n)) {
2131
+ return fallback;
2132
+ }
2133
+ return Math.max(min, Math.min(max, Math.floor(n)));
2134
+ }
2135
+ function resolveUpstreamRetryPolicy(account) {
2136
+ const maxAttempts = clampInt2(
2137
+ account.config.upstreamRetryMaxAttempts,
2138
+ DEFAULT_UPSTREAM_RETRY_MAX_ATTEMPTS,
2139
+ 1,
2140
+ 5
2141
+ );
2142
+ const baseDelayMs = clampInt2(
2143
+ account.config.upstreamRetryBaseDelayMs,
2144
+ DEFAULT_UPSTREAM_RETRY_BASE_DELAY_MS,
2145
+ 0,
2146
+ 1e4
2147
+ );
2148
+ const maxDelayMs = clampInt2(
2149
+ account.config.upstreamRetryMaxDelayMs,
2150
+ DEFAULT_UPSTREAM_RETRY_MAX_DELAY_MS,
2151
+ baseDelayMs,
2152
+ 3e4
2153
+ );
2154
+ return {
2155
+ maxAttempts,
2156
+ baseDelayMs,
2157
+ maxDelayMs
2158
+ };
2159
+ }
2160
+ function isRetryableGuardedReply(guarded) {
2161
+ if (!guarded) {
2162
+ return false;
2163
+ }
2164
+ return guarded.code === "upstream_network_error" || guarded.code === "upstream_timeout";
2165
+ }
2166
+ function resolveUpstreamRetryDelayMs(policy, attempt) {
2167
+ if (attempt <= 0) {
2168
+ return 0;
2169
+ }
2170
+ const exponent = Math.max(0, attempt - 1);
2171
+ const delay = policy.baseDelayMs * 2 ** exponent;
2172
+ return Math.min(policy.maxDelayMs, Math.floor(delay));
2173
+ }
2174
+
2175
+ // src/runtime.ts
2176
+ var runtime = null;
2177
+ function setAibotRuntime(next) {
2178
+ runtime = next;
2179
+ }
2180
+ function getAibotRuntime() {
2181
+ if (!runtime) {
2182
+ throw new Error("Aibot runtime not initialized");
2183
+ }
2184
+ return runtime;
2185
+ }
2186
+
2187
+ // src/quoted-reply-body.ts
2188
+ function buildBodyWithQuotedReplyId(rawBody, quotedMessageId) {
2189
+ if (!quotedMessageId) {
2190
+ return rawBody;
2191
+ }
2192
+ return `[quoted_message_id=${quotedMessageId}]
2193
+ ${rawBody}`;
2194
+ }
2195
+
2196
+ // src/inbound-event-dedupe.ts
2197
+ var DEFAULT_INBOUND_EVENT_TTL_MS = 10 * 60 * 1e3;
2198
+ var recentInboundEvents = /* @__PURE__ */ new Map();
2199
+ function normalizeKeyPart(value) {
2200
+ return String(value ?? "").trim();
2201
+ }
2202
+ function resolveInboundEventKey(params) {
2203
+ const accountId = normalizeKeyPart(params.accountId);
2204
+ const eventId = normalizeKeyPart(params.eventId);
2205
+ if (eventId) {
2206
+ return `account:${accountId}:event:${eventId}`;
2207
+ }
2208
+ const sessionId = normalizeKeyPart(params.sessionId);
2209
+ const messageSid = normalizeKeyPart(params.messageSid);
2210
+ return `account:${accountId}:message:${sessionId}:${messageSid}`;
2211
+ }
2212
+ function resolveTTL(ttlMs) {
2213
+ const normalized = Number(ttlMs);
2214
+ if (!Number.isFinite(normalized) || normalized <= 0) {
2215
+ return DEFAULT_INBOUND_EVENT_TTL_MS;
2216
+ }
2217
+ return Math.floor(normalized);
2218
+ }
2219
+ function pruneExpiredInboundEvents(nowMs) {
2220
+ for (const [key, record] of recentInboundEvents.entries()) {
2221
+ if (record.expiresAt <= nowMs) {
2222
+ recentInboundEvents.delete(key);
2223
+ }
2224
+ }
2225
+ }
2226
+ function claimInboundEvent(params) {
2227
+ const nowMs = Number.isFinite(Number(params.nowMs)) ? Math.floor(Number(params.nowMs)) : Date.now();
2228
+ const ttlMs = resolveTTL(params.ttlMs);
2229
+ pruneExpiredInboundEvents(nowMs);
2230
+ const key = resolveInboundEventKey(params);
2231
+ const existing = recentInboundEvents.get(key);
2232
+ if (existing && existing.expiresAt > nowMs) {
2233
+ return {
2234
+ duplicate: true,
2235
+ confirmed: existing.confirmed,
2236
+ claim: {
2237
+ key,
2238
+ confirmed: existing.confirmed
2239
+ }
2240
+ };
2241
+ }
2242
+ recentInboundEvents.set(key, {
2243
+ expiresAt: nowMs + ttlMs,
2244
+ confirmed: false
2245
+ });
2246
+ return {
2247
+ duplicate: false,
2248
+ confirmed: false,
2249
+ claim: {
2250
+ key,
2251
+ confirmed: false
2252
+ }
2253
+ };
2254
+ }
2255
+ function confirmInboundEvent(claim, params) {
2256
+ const key = normalizeKeyPart(claim.key);
2257
+ if (!key) {
2258
+ return;
2259
+ }
2260
+ const nowMs = Number.isFinite(Number(params?.nowMs)) ? Math.floor(Number(params?.nowMs)) : Date.now();
2261
+ const ttlMs = resolveTTL(params?.ttlMs);
2262
+ pruneExpiredInboundEvents(nowMs);
2263
+ recentInboundEvents.set(key, {
2264
+ expiresAt: nowMs + ttlMs,
2265
+ confirmed: true
2266
+ });
2267
+ claim.confirmed = true;
2268
+ }
2269
+ function releaseInboundEvent(claim) {
2270
+ const key = normalizeKeyPart(claim.key);
2271
+ if (!key || claim.confirmed) {
2272
+ return;
2273
+ }
2274
+ const current = recentInboundEvents.get(key);
2275
+ if (current && !current.confirmed) {
2276
+ recentInboundEvents.delete(key);
2277
+ }
2278
+ }
2279
+
2280
+ // src/exec-approval-card.ts
2281
+ var BIZ_CARD_EXTRA_KEY = "biz_card";
2282
+ var BIZ_CARD_VERSION = 1;
2283
+ var EXEC_APPROVAL_CARD_TYPE = "exec_approval";
2284
+ function normalizeDecision(value) {
2285
+ const normalized = String(value ?? "").trim();
2286
+ if (normalized === "allow-once" || normalized === "allow-always" || normalized === "deny") {
2287
+ return normalized;
2288
+ }
2289
+ return void 0;
2290
+ }
2291
+ function normalizeText2(value) {
2292
+ return String(value ?? "").replace(/\r\n/g, "\n").trim();
2293
+ }
2294
+ function summarizeTextPrefix(text) {
2295
+ const normalized = normalizeText2(text);
2296
+ if (!normalized) {
2297
+ return "";
2298
+ }
2299
+ const firstLine = normalized.split("\n", 1)[0]?.trim() ?? "";
2300
+ if (!firstLine) {
2301
+ return "";
2302
+ }
2303
+ return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
2304
+ }
2305
+ function getExecApprovalReplyMetadata(payload) {
2306
+ const channelData = payload.channelData;
2307
+ if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
2308
+ return null;
2309
+ }
2310
+ const execApproval = channelData.execApproval;
2311
+ if (!execApproval || typeof execApproval !== "object" || Array.isArray(execApproval)) {
2312
+ return null;
2313
+ }
2314
+ const record = execApproval;
2315
+ const approvalId = normalizeText2(record.approvalId);
2316
+ const approvalSlug = normalizeText2(record.approvalSlug);
2317
+ if (!approvalId || !approvalSlug) {
2318
+ return null;
2319
+ }
2320
+ const allowedDecisions = Array.isArray(record.allowedDecisions) ? record.allowedDecisions.map(normalizeDecision).filter((value) => Boolean(value)) : [];
2321
+ return {
2322
+ approvalId,
2323
+ approvalSlug,
2324
+ allowedDecisions: allowedDecisions.length > 0 ? allowedDecisions : ["allow-once", "allow-always", "deny"]
2325
+ };
2326
+ }
2327
+ function getStructuredClawpoolExecApproval(payload) {
2328
+ const channelData = payload.channelData;
2329
+ if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
2330
+ return null;
2331
+ }
2332
+ const clawpool = channelData.clawpool;
2333
+ if (!clawpool || typeof clawpool !== "object" || Array.isArray(clawpool)) {
2334
+ return null;
2335
+ }
2336
+ const execApproval = clawpool.execApproval;
2337
+ if (!execApproval || typeof execApproval !== "object" || Array.isArray(execApproval)) {
2338
+ return null;
2339
+ }
2340
+ const record = execApproval;
2341
+ const approvalCommandId = normalizeText2(record.approval_command_id);
2342
+ const command = normalizeText2(record.command);
2343
+ const host = normalizeText2(record.host);
2344
+ if (!approvalCommandId || !command || !host) {
2345
+ return null;
2346
+ }
2347
+ const expiresValue = Number(record.expires_in_seconds);
2348
+ return {
2349
+ approvalCommandId,
2350
+ command,
2351
+ host,
2352
+ nodeId: normalizeText2(record.node_id) || void 0,
2353
+ cwd: normalizeText2(record.cwd) || void 0,
2354
+ expiresInSeconds: Number.isFinite(expiresValue) && expiresValue >= 0 ? Math.floor(expiresValue) : void 0,
2355
+ warningText: normalizeText2(record.warning_text) || void 0
2356
+ };
2357
+ }
2358
+ function diagnoseExecApprovalPayload(payload) {
2359
+ const rawText = String(payload.text ?? "");
2360
+ const textPrefix = summarizeTextPrefix(rawText);
2361
+ const channelData = payload.channelData;
2362
+ const hasChannelData = Boolean(channelData) && typeof channelData === "object" && !Array.isArray(channelData);
2363
+ const execApproval = hasChannelData ? channelData.execApproval : void 0;
2364
+ const hasExecApprovalField = Boolean(execApproval) && typeof execApproval === "object" && !Array.isArray(execApproval);
2365
+ const execApprovalRecord = hasExecApprovalField ? execApproval : void 0;
2366
+ const approvalId = normalizeText2(execApprovalRecord?.approvalId);
2367
+ const approvalSlug = normalizeText2(execApprovalRecord?.approvalSlug);
2368
+ const allowedDecisionCount = Array.isArray(execApprovalRecord?.allowedDecisions) ? execApprovalRecord.allowedDecisions.length : 0;
2369
+ const structured = getStructuredClawpoolExecApproval(payload);
2370
+ const hasClawpoolApprovalField = Boolean(structured);
2371
+ const isCandidate = hasExecApprovalField || hasClawpoolApprovalField;
2372
+ if (!isCandidate) {
2373
+ return {
2374
+ isCandidate: false,
2375
+ matched: false,
2376
+ reason: "non-approval-payload",
2377
+ hasChannelData,
2378
+ hasExecApprovalField,
2379
+ hasClawpoolApprovalField,
2380
+ allowedDecisionCount,
2381
+ commandDetected: false,
2382
+ textPrefix
2383
+ };
2384
+ }
2385
+ if (!hasChannelData) {
2386
+ return {
2387
+ isCandidate: true,
2388
+ matched: false,
2389
+ reason: "missing-channel-data",
2390
+ hasChannelData,
2391
+ hasExecApprovalField,
2392
+ hasClawpoolApprovalField,
2393
+ allowedDecisionCount,
2394
+ commandDetected: false,
2395
+ textPrefix
2396
+ };
2397
+ }
2398
+ if (!hasExecApprovalField) {
2399
+ return {
2400
+ isCandidate: true,
2401
+ matched: false,
2402
+ reason: "missing-exec-approval-channel-data",
2403
+ hasChannelData,
2404
+ hasExecApprovalField,
2405
+ hasClawpoolApprovalField,
2406
+ allowedDecisionCount,
2407
+ commandDetected: Boolean(structured?.command),
2408
+ approvalCommandId: structured?.approvalCommandId,
2409
+ host: structured?.host,
2410
+ nodeId: structured?.nodeId,
2411
+ cwd: structured?.cwd,
2412
+ expiresInSeconds: structured?.expiresInSeconds,
2413
+ textPrefix
2414
+ };
2415
+ }
2416
+ if (!structured) {
2417
+ return {
2418
+ isCandidate: true,
2419
+ matched: false,
2420
+ reason: "missing-clawpool-channel-data",
2421
+ hasChannelData,
2422
+ hasExecApprovalField,
2423
+ hasClawpoolApprovalField,
2424
+ approvalId: approvalId || void 0,
2425
+ approvalSlug: approvalSlug || void 0,
2426
+ allowedDecisionCount,
2427
+ commandDetected: false,
2428
+ textPrefix
2429
+ };
2430
+ }
2431
+ if (!approvalId || !approvalSlug) {
2432
+ return {
2433
+ isCandidate: true,
2434
+ matched: false,
2435
+ reason: "missing-approval-identifiers",
2436
+ hasChannelData,
2437
+ hasExecApprovalField,
2438
+ hasClawpoolApprovalField,
2439
+ approvalId: approvalId || void 0,
2440
+ approvalSlug: approvalSlug || void 0,
2441
+ allowedDecisionCount,
2442
+ approvalCommandId: structured.approvalCommandId,
2443
+ commandDetected: Boolean(structured.command),
2444
+ host: structured.host || void 0,
2445
+ nodeId: structured.nodeId,
2446
+ cwd: structured.cwd,
2447
+ expiresInSeconds: structured.expiresInSeconds,
2448
+ textPrefix
2449
+ };
2450
+ }
2451
+ if (!structured.approvalCommandId) {
2452
+ return {
2453
+ isCandidate: true,
2454
+ matched: false,
2455
+ reason: "missing-approval-command-id",
2456
+ hasChannelData,
2457
+ hasExecApprovalField,
2458
+ hasClawpoolApprovalField,
2459
+ approvalId,
2460
+ approvalSlug,
2461
+ allowedDecisionCount,
2462
+ commandDetected: Boolean(structured.command),
2463
+ host: structured.host || void 0,
2464
+ nodeId: structured.nodeId,
2465
+ cwd: structured.cwd,
2466
+ expiresInSeconds: structured.expiresInSeconds,
2467
+ textPrefix
2468
+ };
2469
+ }
2470
+ if (!structured.command) {
2471
+ return {
2472
+ isCandidate: true,
2473
+ matched: false,
2474
+ reason: "missing-pending-command",
2475
+ hasChannelData,
2476
+ hasExecApprovalField,
2477
+ hasClawpoolApprovalField,
2478
+ approvalId,
2479
+ approvalSlug,
2480
+ allowedDecisionCount,
2481
+ approvalCommandId: structured.approvalCommandId,
2482
+ commandDetected: false,
2483
+ host: structured.host || void 0,
2484
+ nodeId: structured.nodeId,
2485
+ cwd: structured.cwd,
2486
+ expiresInSeconds: structured.expiresInSeconds,
2487
+ textPrefix
2488
+ };
2489
+ }
2490
+ if (!structured.host) {
2491
+ return {
2492
+ isCandidate: true,
2493
+ matched: false,
2494
+ reason: "missing-host",
2495
+ hasChannelData,
2496
+ hasExecApprovalField,
2497
+ hasClawpoolApprovalField,
2498
+ approvalId,
2499
+ approvalSlug,
2500
+ allowedDecisionCount,
2501
+ approvalCommandId: structured.approvalCommandId,
2502
+ commandDetected: true,
2503
+ nodeId: structured.nodeId,
2504
+ cwd: structured.cwd,
2505
+ expiresInSeconds: structured.expiresInSeconds,
2506
+ textPrefix
2507
+ };
2508
+ }
2509
+ return {
2510
+ isCandidate: true,
2511
+ matched: true,
2512
+ reason: "ok",
2513
+ hasChannelData,
2514
+ hasExecApprovalField,
2515
+ hasClawpoolApprovalField,
2516
+ approvalId,
2517
+ approvalSlug,
2518
+ allowedDecisionCount,
2519
+ approvalCommandId: structured.approvalCommandId,
2520
+ commandDetected: true,
2521
+ host: structured.host,
2522
+ nodeId: structured.nodeId,
2523
+ cwd: structured.cwd,
2524
+ expiresInSeconds: structured.expiresInSeconds,
2525
+ textPrefix
2526
+ };
2527
+ }
2528
+ function buildExecApprovalFallbackText(params) {
2529
+ const compactCommand = params.command.replace(/\s+/g, " ").trim();
2530
+ const summaryCommand = compactCommand.length > 160 ? `${compactCommand.slice(0, 157)}...` : compactCommand;
2531
+ return `[Exec Approval] ${summaryCommand} (${params.host})
2532
+ /approve ${params.approvalCommandId} allow-once`;
2533
+ }
2534
+ function buildExecApprovalCardEnvelope(payload) {
2535
+ const metadata = getExecApprovalReplyMetadata(payload);
2536
+ const structured = getStructuredClawpoolExecApproval(payload);
2537
+ if (!metadata || !structured) {
2538
+ return void 0;
2539
+ }
2540
+ const cardPayload = {
2541
+ approval_id: metadata.approvalId,
2542
+ approval_slug: metadata.approvalSlug,
2543
+ approval_command_id: structured.approvalCommandId,
2544
+ command: structured.command,
2545
+ host: structured.host,
2546
+ allowed_decisions: metadata.allowedDecisions
2547
+ };
2548
+ if (structured.nodeId) {
2549
+ cardPayload.node_id = structured.nodeId;
2550
+ }
2551
+ if (structured.cwd) {
2552
+ cardPayload.cwd = structured.cwd;
2553
+ }
2554
+ if (structured.warningText) {
2555
+ cardPayload.warning_text = structured.warningText;
2556
+ }
2557
+ if (structured.expiresInSeconds !== void 0) {
2558
+ cardPayload.expires_in_seconds = structured.expiresInSeconds;
2559
+ }
2560
+ return {
2561
+ extra: {
2562
+ [BIZ_CARD_EXTRA_KEY]: {
2563
+ version: BIZ_CARD_VERSION,
2564
+ type: EXEC_APPROVAL_CARD_TYPE,
2565
+ payload: cardPayload
2566
+ },
2567
+ channel_data: payload.channelData ?? {}
2568
+ },
2569
+ fallbackText: buildExecApprovalFallbackText({
2570
+ approvalCommandId: structured.approvalCommandId,
2571
+ command: structured.command,
2572
+ host: structured.host
2573
+ })
2574
+ };
2575
+ }
2576
+
2577
+ // src/exec-status-card.ts
2578
+ var BIZ_CARD_EXTRA_KEY2 = "biz_card";
2579
+ var BIZ_CARD_VERSION2 = 1;
2580
+ var EXEC_STATUS_CARD_TYPE = "exec_status";
2581
+ function normalizeText3(value) {
2582
+ return String(value ?? "").replace(/\r\n/g, "\n").trim();
2583
+ }
2584
+ function stripUndefinedFields2(record) {
2585
+ const next = {};
2586
+ for (const [key, value] of Object.entries(record)) {
2587
+ if (value !== void 0) {
2588
+ next[key] = value;
2589
+ }
2590
+ }
2591
+ return next;
2592
+ }
2593
+ function buildExecStatusFallbackText(parsed) {
2594
+ const summary = parsed.summary.replace(/\s+/g, " ").trim();
2595
+ const compactSummary = summary.length > 180 ? `${summary.slice(0, 177)}...` : summary;
2596
+ return `[Exec Status] ${compactSummary}`;
2597
+ }
2598
+ function buildExecStatusExtra(parsed) {
2599
+ return {
2600
+ [BIZ_CARD_EXTRA_KEY2]: {
2601
+ version: BIZ_CARD_VERSION2,
2602
+ type: EXEC_STATUS_CARD_TYPE,
2603
+ payload: stripUndefinedFields2(parsed)
2604
+ },
2605
+ channel_data: {
2606
+ clawpool: {
2607
+ execStatus: stripUndefinedFields2(parsed)
2608
+ }
2609
+ }
2610
+ };
2611
+ }
2612
+ function parseStructuredExecStatus(payload) {
2613
+ const channelData = payload.channelData;
2614
+ if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
2615
+ return null;
2616
+ }
2617
+ const clawpool = channelData.clawpool;
2618
+ if (!clawpool || typeof clawpool !== "object" || Array.isArray(clawpool)) {
2619
+ return null;
2620
+ }
2621
+ const execStatus = clawpool.execStatus;
2622
+ if (!execStatus || typeof execStatus !== "object" || Array.isArray(execStatus)) {
2623
+ return null;
2624
+ }
2625
+ const record = execStatus;
2626
+ const status = normalizeText3(record.status);
2627
+ const summary = normalizeText3(record.summary);
2628
+ const allowedStatuses = /* @__PURE__ */ new Set([
2629
+ "approval-expired",
2630
+ "approval-forwarded",
2631
+ "approval-unavailable",
2632
+ "resolved-allow-once",
2633
+ "resolved-allow-always",
2634
+ "resolved-deny",
2635
+ "running",
2636
+ "finished",
2637
+ "denied"
2638
+ ]);
2639
+ if (!allowedStatuses.has(status) || !summary) {
2640
+ return null;
2641
+ }
2642
+ return stripUndefinedFields2({
2643
+ status,
2644
+ summary,
2645
+ detail_text: normalizeText3(record.detail_text) || void 0,
2646
+ approval_id: normalizeText3(record.approval_id) || void 0,
2647
+ approval_command_id: normalizeText3(record.approval_command_id) || void 0,
2648
+ host: normalizeText3(record.host) || void 0,
2649
+ node_id: normalizeText3(record.node_id) || void 0,
2650
+ session_id: normalizeText3(record.session_id) || void 0,
2651
+ reason: normalizeText3(record.reason) || void 0,
2652
+ decision: record.decision === "allow-once" || record.decision === "allow-always" || record.decision === "deny" ? record.decision : void 0,
2653
+ resolved_by_id: normalizeText3(record.resolved_by_id) || void 0,
2654
+ command: normalizeText3(record.command) || void 0,
2655
+ exit_label: normalizeText3(record.exit_label) || void 0,
2656
+ channel_label: normalizeText3(record.channel_label) || void 0,
2657
+ warning_text: normalizeText3(record.warning_text) || void 0
2658
+ });
2659
+ }
2660
+ function buildExecStatusCardEnvelope(payload) {
2661
+ const parsed = parseStructuredExecStatus(payload);
2662
+ if (!parsed) {
2663
+ return void 0;
2664
+ }
2665
+ return {
2666
+ extra: buildExecStatusExtra(parsed),
2667
+ fallbackText: buildExecStatusFallbackText(parsed)
2668
+ };
2669
+ }
2670
+ function buildExecApprovalResolutionReply(params) {
2671
+ const decisionLabel2 = params.decision === "allow-once" ? "Allow once" : params.decision === "allow-always" ? "Allow always" : "Deny";
2672
+ const actorId = params.actorId.trim() || "unknown";
2673
+ const summary = `${decisionLabel2} selected by ${actorId}.`;
2674
+ const detailText = params.reason?.trim() ? `Reason: ${params.reason.trim()}` : void 0;
2675
+ const payload = stripUndefinedFields2({
2676
+ status: params.decision === "allow-once" ? "resolved-allow-once" : params.decision === "allow-always" ? "resolved-allow-always" : "resolved-deny",
2677
+ summary,
2678
+ detail_text: detailText,
2679
+ approval_id: params.approvalId.trim(),
2680
+ approval_command_id: params.approvalCommandId.trim(),
2681
+ decision: params.decision,
2682
+ reason: params.reason?.trim() || void 0,
2683
+ resolved_by_id: actorId
2684
+ });
2685
+ return {
2686
+ extra: buildExecStatusExtra(payload),
2687
+ fallbackText: buildExecStatusFallbackText(payload)
2688
+ };
2689
+ }
2690
+
2691
+ // src/outbound-envelope.ts
2692
+ function buildAibotOutboundEnvelope(payload) {
2693
+ const execApprovalDiagnostic = diagnoseExecApprovalPayload(payload);
2694
+ const execApprovalCard = buildExecApprovalCardEnvelope(payload);
2695
+ const execStatusCard = execApprovalCard ? void 0 : buildExecStatusCardEnvelope(payload);
2696
+ const envelope = execApprovalCard ?? execStatusCard;
2697
+ return {
2698
+ text: envelope?.fallbackText ?? String(payload.text ?? ""),
2699
+ extra: envelope?.extra,
2700
+ cardKind: execApprovalCard ? "exec_approval" : execStatusCard ? "exec_status" : void 0,
2701
+ execApprovalDiagnostic
2702
+ };
2703
+ }
2704
+
2705
+ // src/exec-approval-command.ts
2706
+ var COMMAND_REGEX = /^\/approve(?:@[^\s]+)?(?:\s|$)/i;
2707
+ var DIRECTIVE_REGEX = /\[\[exec-approval-resolution\|(.+?)\]\]/i;
2708
+ var DECISION_ALIASES = {
2709
+ allow: "allow-once",
2710
+ once: "allow-once",
2711
+ "allow-once": "allow-once",
2712
+ allowonce: "allow-once",
2713
+ always: "allow-always",
2714
+ "allow-always": "allow-always",
2715
+ allowalways: "allow-always",
2716
+ deny: "deny",
2717
+ reject: "deny",
2718
+ block: "deny"
2719
+ };
2720
+ var EXEC_APPROVAL_USAGE = "Usage: /approve <id> allow-once|allow-always|deny";
2721
+ function decodeDirectiveValue(rawValue) {
2722
+ const normalized = rawValue.trim();
2723
+ if (!normalized) {
2724
+ return void 0;
2725
+ }
2726
+ if (!normalized.includes("%")) {
2727
+ return normalized;
2728
+ }
2729
+ try {
2730
+ return decodeURIComponent(normalized);
2731
+ } catch {
2732
+ return normalized;
2733
+ }
2734
+ }
2735
+ function parseExecApprovalResolutionDirective(raw) {
2736
+ const match = DIRECTIVE_REGEX.exec(String(raw ?? ""));
2737
+ if (!match) {
2738
+ return { matched: false };
2739
+ }
2740
+ const body = String(match[1] ?? "").trim();
2741
+ if (!body) {
2742
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2743
+ }
2744
+ const payload = /* @__PURE__ */ new Map();
2745
+ for (const segment of body.split("|")) {
2746
+ const normalizedSegment = segment.trim();
2747
+ if (!normalizedSegment) {
2748
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2749
+ }
2750
+ const separatorIndex = normalizedSegment.indexOf("=");
2751
+ if (separatorIndex <= 0 || separatorIndex >= normalizedSegment.length - 1) {
2752
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2753
+ }
2754
+ const key = normalizedSegment.slice(0, separatorIndex).trim();
2755
+ const rawValue = normalizedSegment.slice(separatorIndex + 1);
2756
+ const value = decodeDirectiveValue(rawValue);
2757
+ if (!key || !value) {
2758
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2759
+ }
2760
+ payload.set(key, value);
2761
+ }
2762
+ const decision = DECISION_ALIASES[String(payload.get("decision") ?? "").toLowerCase()];
2763
+ const approvalId = String(payload.get("approval_id") ?? "").trim() || void 0;
2764
+ const approvalCommandId = String(
2765
+ payload.get("approval_command_id") ?? payload.get("approval_id") ?? payload.get("approval_slug") ?? ""
2766
+ ).trim();
2767
+ if (!decision || !approvalCommandId) {
2768
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2769
+ }
2770
+ const reason = String(payload.get("reason") ?? "").trim() || void 0;
2771
+ return {
2772
+ matched: true,
2773
+ ok: true,
2774
+ id: approvalCommandId,
2775
+ approvalId,
2776
+ approvalCommandId,
2777
+ decision,
2778
+ reason
2779
+ };
2780
+ }
2781
+ function parseExecApprovalCommand(raw) {
2782
+ const directiveParsed = parseExecApprovalResolutionDirective(raw);
2783
+ if (directiveParsed.matched) {
2784
+ return directiveParsed;
2785
+ }
2786
+ const trimmed = String(raw ?? "").trim();
2787
+ const commandMatch = trimmed.match(COMMAND_REGEX);
2788
+ if (!commandMatch) {
2789
+ return { matched: false };
2790
+ }
2791
+ const rest = trimmed.slice(commandMatch[0].length).trim();
2792
+ if (!rest) {
2793
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2794
+ }
2795
+ const tokens = rest.split(/\s+/).filter(Boolean);
2796
+ if (tokens.length < 2) {
2797
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2798
+ }
2799
+ const first = tokens[0].toLowerCase();
2800
+ const second = tokens[1].toLowerCase();
2801
+ const firstDecision = DECISION_ALIASES[first];
2802
+ if (firstDecision) {
2803
+ const id = tokens.slice(1).join(" ").trim();
2804
+ if (!id) {
2805
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2806
+ }
2807
+ return {
2808
+ matched: true,
2809
+ ok: true,
2810
+ decision: firstDecision,
2811
+ id,
2812
+ approvalCommandId: id
2813
+ };
2814
+ }
2815
+ const secondDecision = DECISION_ALIASES[second];
2816
+ if (!secondDecision) {
2817
+ return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
2818
+ }
2819
+ return {
2820
+ matched: true,
2821
+ ok: true,
2822
+ decision: secondDecision,
2823
+ id: tokens[0],
2824
+ approvalCommandId: tokens[0]
2825
+ };
2826
+ }
2827
+
2828
+ // src/exec-approvals.ts
2829
+ function normalizeExecApprovalConfig(config) {
2830
+ const approvers = (config?.approvers ?? []).map((value) => String(value ?? "").trim()).filter(Boolean);
2831
+ return {
2832
+ enabled: Boolean(config?.enabled && approvers.length > 0),
2833
+ approvers
2834
+ };
2835
+ }
2836
+ function formatCommandFailure(result) {
2837
+ const parts = [String(result.stderr ?? "").trim(), String(result.stdout ?? "").trim()].filter(Boolean).flatMap((text) => text.split(/\r?\n/)).map((line) => line.trim()).filter(Boolean);
2838
+ if (parts.length > 0) {
2839
+ return parts.at(-1) ?? "unknown error";
2840
+ }
2841
+ if (result.signal) {
2842
+ return `signal=${result.signal}`;
2843
+ }
2844
+ if (result.code !== null) {
2845
+ return `exit code ${result.code}`;
2846
+ }
2847
+ return result.termination || "unknown failure";
2848
+ }
2849
+ function resolveOpenClawCliArgvPrefix() {
2850
+ const execPath = String(process.execPath ?? "").trim();
2851
+ const scriptPath = String(process.argv[1] ?? "").trim();
2852
+ if (execPath && scriptPath) {
2853
+ return [execPath, scriptPath];
2854
+ }
2855
+ return ["openclaw"];
2856
+ }
2857
+ function buildExecApprovalResolveArgv(params) {
2858
+ const cliArgvPrefix = params.cliArgvPrefix && params.cliArgvPrefix.length > 0 ? params.cliArgvPrefix : resolveOpenClawCliArgvPrefix();
2859
+ const timeoutMs = Math.max(1e3, Math.floor(params.timeoutMs ?? 15e3));
2860
+ return [
2861
+ ...cliArgvPrefix,
2862
+ "gateway",
2863
+ "call",
2864
+ "exec.approval.resolve",
2865
+ "--json",
2866
+ "--timeout",
2867
+ String(timeoutMs),
2868
+ "--params",
2869
+ JSON.stringify({
2870
+ id: params.id,
2871
+ decision: params.decision
2872
+ })
2873
+ ];
2874
+ }
2875
+ async function submitExecApprovalDecision(params) {
2876
+ const runner = params.runner ?? params.runtime.system.runCommandWithTimeout;
2877
+ const timeoutMs = Math.max(1e3, Math.floor(params.timeoutMs ?? 15e3));
2878
+ const argv = buildExecApprovalResolveArgv({
2879
+ cliArgvPrefix: params.cliArgvPrefix,
2880
+ id: params.id,
2881
+ decision: params.decision,
2882
+ timeoutMs
2883
+ });
2884
+ const result = await runner(argv, { timeoutMs });
2885
+ if (result.termination !== "exit" || result.code !== 0) {
2886
+ throw new Error(formatCommandFailure(result));
2887
+ }
2888
+ }
2889
+ function isExecApprovalApprover(params) {
2890
+ const senderId = String(params.senderId ?? "").trim();
2891
+ if (!senderId) {
2892
+ return false;
2893
+ }
2894
+ const config = normalizeExecApprovalConfig(params.account.config.execApprovals);
2895
+ if (!config.enabled) {
2896
+ return false;
2897
+ }
2898
+ return config.approvers.includes(senderId);
2899
+ }
2900
+ function disabledReplyText(accountId) {
2901
+ return `\u274C ClawPool exec approvals are not enabled for account ${accountId}.`;
2902
+ }
2903
+ function unauthorizedReplyText() {
2904
+ return "\u274C You are not authorized to approve exec requests on ClawPool.";
2905
+ }
2906
+ function successReplyText(command) {
2907
+ return `\u2705 Exec approval ${command.decision} submitted for ${command.id}.`;
2908
+ }
2909
+ function failureReplyText(message) {
2910
+ return `\u274C Failed to submit approval: ${message}`;
2911
+ }
2912
+ async function handleExecApprovalCommand(params) {
2913
+ const parsed = parseExecApprovalCommand(params.rawBody);
2914
+ if (!parsed.matched) {
2915
+ return { handled: false };
2916
+ }
2917
+ if (!parsed.ok) {
2918
+ return {
2919
+ handled: true,
2920
+ replyText: parsed.error
2921
+ };
2922
+ }
2923
+ const config = normalizeExecApprovalConfig(params.account.config.execApprovals);
2924
+ if (!config.enabled) {
2925
+ return {
2926
+ handled: true,
2927
+ replyText: disabledReplyText(params.account.accountId)
2928
+ };
2929
+ }
2930
+ if (!isExecApprovalApprover({ account: params.account, senderId: params.senderId })) {
2931
+ return {
2932
+ handled: true,
2933
+ replyText: unauthorizedReplyText()
2934
+ };
2935
+ }
2936
+ try {
2937
+ await submitExecApprovalDecision({
2938
+ runtime: params.runtime,
2939
+ id: parsed.id,
2940
+ decision: parsed.decision,
2941
+ timeoutMs: params.timeoutMs,
2942
+ runner: params.runner,
2943
+ cliArgvPrefix: params.cliArgvPrefix
2944
+ });
2945
+ const actorId = String(params.senderId ?? "").trim();
2946
+ const approvalId = String(parsed.approvalId ?? "").trim();
2947
+ return {
2948
+ handled: true,
2949
+ replyText: successReplyText(parsed),
2950
+ ...approvalId ? {
2951
+ replyExtra: buildExecApprovalResolutionReply({
2952
+ approvalId,
2953
+ approvalCommandId: parsed.approvalCommandId,
2954
+ decision: parsed.decision,
2955
+ actorId: actorId || "unknown",
2956
+ reason: parsed.reason
2957
+ }).extra
2958
+ } : {}
2959
+ };
2960
+ } catch (err) {
2961
+ return {
2962
+ handled: true,
2963
+ replyText: failureReplyText(err instanceof Error ? err.message : String(err))
2964
+ };
2965
+ }
2966
+ }
2967
+
2968
+ // src/revoke-event.ts
2969
+ function toStringId(value) {
2970
+ return String(value ?? "").trim();
2971
+ }
2972
+ function resolveChatType(sessionType) {
2973
+ if (sessionType === 1) {
2974
+ return "direct";
2975
+ }
2976
+ if (sessionType === 2) {
2977
+ return "group";
2978
+ }
2979
+ throw new Error(`clawpool revoke event has unsupported session_type=${sessionType}`);
2980
+ }
2981
+ function enqueueRevokeSystemEvent(params) {
2982
+ const sessionId = toStringId(params.event.session_id);
2983
+ const messageId = toStringId(params.event.msg_id);
2984
+ const senderId = toStringId(params.event.sender_id);
2985
+ const sessionType = Number(params.event.session_type);
2986
+ if (!sessionId || !messageId) {
2987
+ throw new Error(
2988
+ `invalid event_revoke payload: session_id=${sessionId || "<empty>"} msg_id=${messageId || "<empty>"}`
2989
+ );
2990
+ }
2991
+ const chatType = resolveChatType(sessionType);
2992
+ const route = params.core.channel.routing.resolveAgentRoute({
2993
+ cfg: params.config,
2994
+ channel: "clawpool",
2995
+ accountId: params.account.accountId,
2996
+ peer: {
2997
+ kind: chatType,
2998
+ id: sessionId
2999
+ }
3000
+ });
3001
+ const metadataParts = [`session_id=${sessionId}`, `msg_id=${messageId}`];
3002
+ if (senderId) {
3003
+ metadataParts.push(`sender_id=${senderId}`);
3004
+ }
3005
+ const text = `Clawpool ${chatType} message deleted [${metadataParts.join(" ")}]`;
3006
+ params.core.system.enqueueSystemEvent(text, {
3007
+ sessionKey: route.sessionKey,
3008
+ contextKey: `clawpool:revoke:${sessionId}:${messageId}`
3009
+ });
3010
+ return {
3011
+ messageId,
3012
+ sessionId,
3013
+ sessionKey: route.sessionKey,
3014
+ text
3015
+ };
3016
+ }
3017
+
3018
+ // src/reply-dispatch-outcome.ts
3019
+ function hasPositiveCount(value) {
3020
+ if (typeof value === "number") {
3021
+ return Number.isFinite(value) && value > 0;
3022
+ }
3023
+ if (Array.isArray(value)) {
3024
+ return value.some(hasPositiveCount);
3025
+ }
3026
+ if (value && typeof value === "object") {
3027
+ return Object.values(value).some(hasPositiveCount);
3028
+ }
3029
+ return false;
3030
+ }
3031
+ function shouldTreatDispatchAsRespondedWithoutVisibleOutput(result) {
3032
+ if (!result || typeof result !== "object") {
3033
+ return false;
3034
+ }
3035
+ const typedResult = result;
3036
+ if (typedResult.queuedFinal === true) {
3037
+ return true;
3038
+ }
3039
+ if (hasPositiveCount(typedResult.counts)) {
3040
+ return true;
3041
+ }
3042
+ return false;
3043
+ }
3044
+
3045
+ // src/aibot-payload-delivery.ts
3046
+ import {
3047
+ resolveOutboundMediaUrls,
3048
+ sendMediaWithLeadingCaption
3049
+ } from "openclaw/plugin-sdk/reply-payload";
3050
+
3051
+ // src/outbound-text-delivery-plan.ts
3052
+ function buildAibotTextSendPlan(params) {
3053
+ const plan = [];
3054
+ let chunkIndex = 0;
3055
+ for (const chunk of params.chunks) {
3056
+ const normalized = String(chunk ?? "");
3057
+ if (!normalized) {
3058
+ continue;
3059
+ }
3060
+ chunkIndex += 1;
3061
+ const clientMsgId = params.stableClientMsgId ? `${params.stableClientMsgId}_chunk${chunkIndex}` : void 0;
3062
+ const extra = chunkIndex === 1 ? params.firstChunkExtra : void 0;
3063
+ plan.push({
3064
+ text: normalized,
3065
+ ...clientMsgId ? { clientMsgId } : {},
3066
+ ...extra ? { extra } : {}
3067
+ });
3068
+ }
3069
+ return plan;
3070
+ }
3071
+
3072
+ // src/aibot-payload-delivery.ts
3073
+ function resolveAckMessageId(ack, fallback) {
3074
+ const raw = ack.msg_id ?? ack.client_msg_id ?? fallback;
3075
+ const normalized = String(raw ?? "").trim();
3076
+ return normalized || void 0;
3077
+ }
3078
+ async function deliverAibotPayload(params) {
3079
+ const mediaUrls = resolveOutboundMediaUrls(params.payload);
3080
+ const textChunks = splitTextForAibotProtocol(
3081
+ params.text,
3082
+ resolveOutboundTextChunkLimit(params.account.config.maxChunkChars)
3083
+ );
3084
+ const textSendPlan = buildAibotTextSendPlan({
3085
+ chunks: textChunks,
3086
+ stableClientMsgId: params.stableClientMsgId,
3087
+ firstChunkExtra: params.extra
3088
+ });
3089
+ let firstMessageId;
3090
+ let sent = false;
3091
+ let notifiedFirstVisibleSend = false;
3092
+ const markVisibleDelivery = () => {
3093
+ if (notifiedFirstVisibleSend) {
3094
+ return;
3095
+ }
3096
+ notifiedFirstVisibleSend = true;
3097
+ params.onFirstVisibleSend?.();
3098
+ };
3099
+ const mediaSent = await sendMediaWithLeadingCaption({
3100
+ mediaUrls,
3101
+ caption: textSendPlan[0]?.text ?? "",
3102
+ send: async ({ mediaUrl, caption }) => {
3103
+ if (params.abortSignal?.aborted) {
3104
+ return;
3105
+ }
3106
+ const ack = await params.client.sendMedia(params.sessionId, mediaUrl, caption ?? "", {
3107
+ eventId: params.eventId,
3108
+ quotedMessageId: params.quotedMessageId,
3109
+ clientMsgId: params.stableClientMsgId ? `${params.stableClientMsgId}_media` : void 0,
3110
+ extra: params.extra
3111
+ });
3112
+ firstMessageId ??= resolveAckMessageId(
3113
+ ack,
3114
+ params.stableClientMsgId ? `${params.stableClientMsgId}_media` : void 0
3115
+ );
3116
+ sent = true;
3117
+ markVisibleDelivery();
3118
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3119
+ },
3120
+ onError: (error) => {
3121
+ params.onMediaError?.(error);
3122
+ params.statusSink?.({ lastError: String(error) });
3123
+ }
3124
+ });
3125
+ if (mediaSent) {
3126
+ for (const chunkPlan of textSendPlan.slice(1)) {
3127
+ if (params.abortSignal?.aborted) {
3128
+ return { sent, firstMessageId };
3129
+ }
3130
+ const ack = await params.client.sendText(params.sessionId, chunkPlan.text, {
3131
+ eventId: params.eventId,
3132
+ quotedMessageId: params.quotedMessageId,
3133
+ clientMsgId: chunkPlan.clientMsgId
3134
+ });
3135
+ firstMessageId ??= resolveAckMessageId(ack, chunkPlan.clientMsgId);
3136
+ sent = true;
3137
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3138
+ }
3139
+ return { sent: true, firstMessageId };
3140
+ }
3141
+ if (textSendPlan.length === 0) {
3142
+ return { sent: false, firstMessageId };
3143
+ }
3144
+ for (const chunkPlan of textSendPlan) {
3145
+ if (params.abortSignal?.aborted) {
3146
+ return { sent, firstMessageId };
3147
+ }
3148
+ const ack = await params.client.sendText(params.sessionId, chunkPlan.text, {
3149
+ eventId: params.eventId,
3150
+ quotedMessageId: params.quotedMessageId,
3151
+ clientMsgId: chunkPlan.clientMsgId,
3152
+ extra: chunkPlan.extra
3153
+ });
3154
+ firstMessageId ??= resolveAckMessageId(ack, chunkPlan.clientMsgId);
3155
+ sent = true;
3156
+ markVisibleDelivery();
3157
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3158
+ }
3159
+ return { sent, firstMessageId };
3160
+ }
3161
+
3162
+ // src/monitor.ts
3163
+ var activeMonitorClients = /* @__PURE__ */ new Map();
3164
+ function registerActiveMonitor(accountId, client) {
3165
+ if (!accountId) {
3166
+ return null;
3167
+ }
3168
+ const previous = activeMonitorClients.get(accountId) ?? null;
3169
+ activeMonitorClients.set(accountId, client);
3170
+ return previous === client ? null : previous;
3171
+ }
3172
+ function isActiveMonitor(accountId, client) {
3173
+ if (!accountId) {
3174
+ return false;
3175
+ }
3176
+ return activeMonitorClients.get(accountId) === client;
3177
+ }
3178
+ function clearActiveMonitor(accountId, client) {
3179
+ if (!accountId) {
3180
+ return;
3181
+ }
3182
+ if (activeMonitorClients.get(accountId) !== client) {
3183
+ return;
3184
+ }
3185
+ activeMonitorClients.delete(accountId);
3186
+ }
3187
+ function toStringId2(value) {
3188
+ const text = String(value ?? "").trim();
3189
+ return text;
3190
+ }
3191
+ function toTimestampMs(value) {
3192
+ const n = Number(value);
3193
+ if (!Number.isFinite(n) || n <= 0) {
3194
+ return void 0;
3195
+ }
3196
+ if (n < 1e12) {
3197
+ return Math.floor(n * 1e3);
3198
+ }
3199
+ return Math.floor(n);
3200
+ }
3201
+ function normalizeNumericMessageId(value) {
3202
+ const raw = toStringId2(value);
3203
+ if (!raw) {
3204
+ return void 0;
3205
+ }
3206
+ return /^\d+$/.test(raw) ? raw : void 0;
3207
+ }
3208
+ function resolveStreamChunkChars(account) {
3209
+ return resolveStreamTextChunkLimit(account.config.streamChunkChars);
3210
+ }
3211
+ function resolveStreamChunkDelayMs(account) {
3212
+ return Math.max(0, Math.floor(account.config.streamChunkDelayMs ?? 0));
3213
+ }
3214
+ function resolveStreamFinishDelayMs(account) {
3215
+ return resolveStreamChunkDelayMs(account);
3216
+ }
3217
+ var composingRenewIntervalMs = 8e3;
3218
+ function sleep2(ms) {
3219
+ if (ms <= 0) {
3220
+ return Promise.resolve();
3221
+ }
3222
+ return new Promise((resolve) => setTimeout(resolve, ms));
3223
+ }
3224
+ function resolveAbortReason(signal) {
3225
+ const reason = String(signal?.reason ?? "").trim();
3226
+ return reason || "-";
3227
+ }
3228
+ function buildEventLogContext(params) {
3229
+ const parts = [
3230
+ `eventId=${params.eventId || "-"}`,
3231
+ `sessionId=${params.sessionId}`,
3232
+ `messageSid=${params.messageSid}`
3233
+ ];
3234
+ if (params.clientMsgId) {
3235
+ parts.push(`clientMsgId=${params.clientMsgId}`);
3236
+ }
3237
+ if (params.outboundCounter !== void 0) {
3238
+ parts.push(`outboundCounter=${params.outboundCounter}`);
3239
+ }
3240
+ return parts.join(" ");
3241
+ }
3242
+ async function deliverAibotStreamBlock(params) {
3243
+ const chunks = splitTextForAibotProtocol(params.text, resolveStreamChunkChars(params.account));
3244
+ const chunkDelayMs = resolveStreamChunkDelayMs(params.account);
3245
+ let didSend = false;
3246
+ const context = buildEventLogContext({
3247
+ eventId: params.eventId,
3248
+ sessionId: params.sessionId,
3249
+ messageSid: params.messageSid,
3250
+ clientMsgId: params.clientMsgId
3251
+ });
3252
+ params.runtime.log(
3253
+ `[clawpool:${params.account.accountId}] stream block split into ${chunks.length} chunk(s) ${context} textLen=${params.text.length} chunkDelayMs=${chunkDelayMs}`
3254
+ );
3255
+ for (let index = 0; index < chunks.length; index++) {
3256
+ if (params.abortSignal?.aborted) {
3257
+ params.runtime.log(
3258
+ `[clawpool:${params.account.accountId}] stream chunk abort before send ${context} chunkIndex=${index + 1}/${chunks.length} didSend=${didSend} abortReason=${resolveAbortReason(params.abortSignal)}`
3259
+ );
3260
+ return didSend;
3261
+ }
3262
+ const chunk = chunks[index];
3263
+ const normalized = String(chunk ?? "");
3264
+ if (!normalized) {
3265
+ continue;
3266
+ }
3267
+ params.runtime.log(
3268
+ `[clawpool:${params.account.accountId}] stream chunk send ${context} chunkIndex=${index + 1}/${chunks.length} deltaLen=${normalized.length}`
3269
+ );
3270
+ await params.client.sendStreamChunk(params.sessionId, normalized, {
3271
+ eventId: params.eventId,
3272
+ clientMsgId: params.clientMsgId,
3273
+ quotedMessageId: params.quotedMessageId,
3274
+ isFinish: false
3275
+ });
3276
+ didSend = true;
3277
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3278
+ if (chunkDelayMs > 0 && index < chunks.length - 1) {
3279
+ await sleep2(chunkDelayMs);
3280
+ }
3281
+ }
3282
+ return didSend;
3283
+ }
3284
+ async function deliverAibotMessage(params) {
3285
+ const { payload, client, account, sessionId, quotedMessageId, runtime: runtime2, statusSink, stableClientMsgId } = params;
3286
+ const core = getAibotRuntime();
3287
+ const tableMode = params.tableMode ?? "code";
3288
+ const outboundEnvelope = buildAibotOutboundEnvelope(payload);
3289
+ const execApprovalDiagnostic = outboundEnvelope.execApprovalDiagnostic;
3290
+ if (execApprovalDiagnostic.isCandidate) {
3291
+ runtime2.log(
3292
+ `[clawpool:${account.accountId}] exec approval outbound diagnostic eventId=${params.eventId || "-"} sessionId=${sessionId} clientMsgId=${stableClientMsgId || "-"} matched=${execApprovalDiagnostic.matched ? "true" : "false"} reason=${execApprovalDiagnostic.reason} hasChannelData=${execApprovalDiagnostic.hasChannelData ? "true" : "false"} hasExecApprovalField=${execApprovalDiagnostic.hasExecApprovalField ? "true" : "false"} approvalId=${execApprovalDiagnostic.approvalId || "-"} approvalSlug=${execApprovalDiagnostic.approvalSlug || "-"} approvalCommandId=${execApprovalDiagnostic.approvalCommandId || "-"} commandDetected=${execApprovalDiagnostic.commandDetected ? "true" : "false"} host=${execApprovalDiagnostic.host || "-"} nodeId=${execApprovalDiagnostic.nodeId || "-"} cwd=${execApprovalDiagnostic.cwd || "-"} expiresInSeconds=${execApprovalDiagnostic.expiresInSeconds ?? "-"} allowedDecisionCount=${execApprovalDiagnostic.allowedDecisionCount} textPrefix=${JSON.stringify(execApprovalDiagnostic.textPrefix)} bizCard=${outboundEnvelope.cardKind ?? "none"}`
3293
+ );
3294
+ }
3295
+ const rawText = outboundEnvelope.text;
3296
+ const text = core.channel.text.convertMarkdownTables(rawText, tableMode);
3297
+ const delivery = await deliverAibotPayload({
3298
+ payload,
3299
+ text,
3300
+ extra: outboundEnvelope.extra,
3301
+ client,
3302
+ account,
3303
+ sessionId,
3304
+ abortSignal: params.abortSignal,
3305
+ eventId: params.eventId,
3306
+ quotedMessageId,
3307
+ stableClientMsgId,
3308
+ onMediaError: (error) => {
3309
+ runtime2.error(`clawpool media send failed: ${String(error)}`);
3310
+ },
3311
+ statusSink
3312
+ });
3313
+ return delivery.sent;
3314
+ }
3315
+ async function bindSessionRouteMapping(params) {
3316
+ const routeSessionKey = String(params.routeSessionKey ?? "").trim();
3317
+ const sessionId = String(params.sessionId ?? "").trim();
3318
+ if (!routeSessionKey || !sessionId) {
3319
+ return;
3320
+ }
3321
+ try {
3322
+ params.runtime.log(
3323
+ `[clawpool:${params.account.accountId}] session route bind begin routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
3324
+ );
3325
+ await params.client.bindSessionRoute(
3326
+ "clawpool",
3327
+ params.account.accountId,
3328
+ routeSessionKey,
3329
+ sessionId
3330
+ );
3331
+ params.runtime.log(
3332
+ `[clawpool:${params.account.accountId}] session route bind success routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
3333
+ );
3334
+ } catch (err) {
3335
+ const reason = `clawpool session route bind failed routeSessionKey=${routeSessionKey} sessionId=${sessionId}: ${String(err)}`;
3336
+ params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
3337
+ params.statusSink?.({ lastError: reason });
3338
+ }
3339
+ }
3340
+ function handleEventStop(params) {
3341
+ const eventId = toStringId2(params.payload.event_id);
3342
+ const sessionId = toStringId2(params.payload.session_id);
3343
+ const stopId = toStringId2(params.payload.stop_id);
3344
+ if (!eventId || !sessionId) {
3345
+ const reason = `invalid event_stop payload: event_id=${eventId || "<empty>"} session_id=${sessionId || "<empty>"}`;
3346
+ params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
3347
+ params.statusSink?.({ lastError: reason });
3348
+ return;
3349
+ }
3350
+ params.runtime.log(
3351
+ `[clawpool:${params.account.accountId}] event_stop begin eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"} acceptedPayload=${JSON.stringify(params.payload)}`
3352
+ );
3353
+ try {
3354
+ params.client.sendEventStopAck({
3355
+ stop_id: stopId,
3356
+ event_id: eventId,
3357
+ accepted: true,
3358
+ updated_at: Date.now()
3359
+ });
3360
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3361
+ params.runtime.log(
3362
+ `[clawpool:${params.account.accountId}] event_stop_ack sent eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"}`
3363
+ );
3364
+ } catch (err) {
3365
+ const reason = `event_stop_ack failed eventId=${eventId} sessionId=${sessionId}: ${String(err)}`;
3366
+ params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
3367
+ params.statusSink?.({ lastError: reason });
3368
+ return;
3369
+ }
3370
+ const activeRun = resolveActiveReplyRun({
3371
+ accountId: params.account.accountId,
3372
+ eventId,
3373
+ sessionId
3374
+ });
3375
+ params.runtime.log(
3376
+ `[clawpool:${params.account.accountId}] event_stop resolve_active_run eventId=${eventId} sessionId=${sessionId} found=${activeRun ? "true" : "false"} stopRequested=${activeRun?.stopRequested === true} aborted=${activeRun?.controller.signal.aborted === true} abortReason=${activeRun ? resolveAbortReason(activeRun.controller.signal) : "-"}`
3377
+ );
3378
+ if (!activeRun) {
3379
+ params.client.sendEventStopResult({
3380
+ stop_id: stopId,
3381
+ event_id: eventId,
3382
+ status: "already_finished",
3383
+ updated_at: Date.now()
3384
+ });
3385
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3386
+ params.runtime.log(
3387
+ `[clawpool:${params.account.accountId}] event_stop already_finished eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"}`
3388
+ );
3389
+ return;
3390
+ }
3391
+ activeRun.stopRequested = true;
3392
+ activeRun.stopId = stopId;
3393
+ activeRun.abortReason = "owner_requested_stop";
3394
+ if (!activeRun.controller.signal.aborted) {
3395
+ activeRun.controller.abort(activeRun.abortReason);
3396
+ }
3397
+ params.runtime.log(
3398
+ `[clawpool:${params.account.accountId}] owner stop requested eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"} aborted=${activeRun.controller.signal.aborted} abortReason=${resolveAbortReason(activeRun.controller.signal)}`
3399
+ );
3400
+ }
3401
+ function reportHandledCommandResult(params) {
3402
+ if (!params.eventId) {
3403
+ return;
3404
+ }
3405
+ try {
3406
+ params.client.sendEventResult({
3407
+ event_id: params.eventId,
3408
+ status: params.status,
3409
+ code: params.code,
3410
+ msg: params.msg,
3411
+ updated_at: Date.now()
3412
+ });
3413
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3414
+ } catch (err) {
3415
+ params.runtime.error(
3416
+ `[clawpool:${params.account.accountId}] command event result send failed eventId=${params.eventId} status=${params.status}: ${String(err)}`
3417
+ );
3418
+ params.statusSink?.({ lastError: String(err) });
3419
+ }
3420
+ }
3421
+ async function sendHandledCommandReply(params) {
3422
+ await params.client.sendText(params.sessionId, params.replyText, {
3423
+ eventId: params.eventId,
3424
+ quotedMessageId: params.quotedMessageId,
3425
+ extra: params.replyExtra
3426
+ });
3427
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3428
+ params.runtime.log(
3429
+ `[clawpool:${params.account.accountId}] command reply sent eventId=${params.eventId || "-"} sessionId=${params.sessionId} quotedMessageId=${params.quotedMessageId || "-"} textLen=${params.replyText.length}`
3430
+ );
3431
+ }
3432
+ async function processEvent(params) {
3433
+ const { event, account, config, runtime: runtime2, client, statusSink } = params;
3434
+ const core = getAibotRuntime();
3435
+ const sessionId = toStringId2(event.session_id);
3436
+ const messageSid = toStringId2(event.msg_id);
3437
+ const rawBody = String(event.content ?? "").trim();
3438
+ if (!sessionId || !messageSid || !rawBody) {
3439
+ const reason = `invalid event_msg payload: session_id=${sessionId || "<empty>"} msg_id=${messageSid || "<empty>"}`;
3440
+ runtime2.error(`[clawpool:${account.accountId}] ${reason}`);
3441
+ statusSink?.({ lastError: reason });
3442
+ return;
3443
+ }
3444
+ const eventId = toStringId2(event.event_id);
3445
+ const quotedMessageId = normalizeNumericMessageId(event.quoted_message_id);
3446
+ const bodyForAgent = buildBodyWithQuotedReplyId(rawBody, quotedMessageId);
3447
+ const senderId = toStringId2(event.sender_id);
3448
+ const isGroup = Number(event.session_type ?? 0) === 2 || String(event.event_type ?? "").startsWith("group_");
3449
+ const chatType = isGroup ? "group" : "direct";
3450
+ const createdAt = toTimestampMs(event.created_at);
3451
+ const baseLogContext = buildEventLogContext({
3452
+ eventId,
3453
+ sessionId,
3454
+ messageSid
3455
+ });
3456
+ let visibleOutputSent = false;
3457
+ const inboundEvent = claimInboundEvent({
3458
+ accountId: account.accountId,
3459
+ eventId,
3460
+ sessionId,
3461
+ messageSid
3462
+ });
3463
+ if (inboundEvent.duplicate) {
3464
+ runtime2.log(
3465
+ `[clawpool:${account.accountId}] skip duplicate inbound event ${baseLogContext} confirmed=${inboundEvent.confirmed}`
3466
+ );
3467
+ if (inboundEvent.confirmed && eventId) {
3468
+ try {
3469
+ client.ackEvent(eventId, {
3470
+ sessionId,
3471
+ msgId: messageSid,
3472
+ receivedAt: Date.now()
3473
+ });
3474
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3475
+ } catch (err) {
3476
+ runtime2.error(
3477
+ `[clawpool:${account.accountId}] duplicate event ack failed eventId=${eventId}: ${String(err)}`
3478
+ );
3479
+ statusSink?.({ lastError: String(err) });
3480
+ }
3481
+ }
3482
+ return;
3483
+ }
3484
+ runtime2.log(
3485
+ `[clawpool:${account.accountId}] inbound event ${baseLogContext} chatType=${chatType} bodyLen=${rawBody.length} quotedMessageId=${quotedMessageId || "-"}`
3486
+ );
3487
+ let inboundEventAccepted = false;
3488
+ const commandOutcome = await handleExecApprovalCommand({
3489
+ rawBody,
3490
+ senderId,
3491
+ account,
3492
+ runtime: core
3493
+ });
3494
+ if (commandOutcome.handled) {
3495
+ try {
3496
+ if (eventId) {
3497
+ client.ackEvent(eventId, {
3498
+ sessionId,
3499
+ msgId: messageSid,
3500
+ receivedAt: Date.now()
3501
+ });
3502
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3503
+ }
3504
+ confirmInboundEvent(inboundEvent.claim);
3505
+ inboundEventAccepted = true;
3506
+ await sendHandledCommandReply({
3507
+ client,
3508
+ sessionId,
3509
+ replyText: commandOutcome.replyText,
3510
+ replyExtra: commandOutcome.replyExtra,
3511
+ eventId,
3512
+ quotedMessageId: normalizeNumericMessageId(messageSid),
3513
+ account,
3514
+ runtime: runtime2,
3515
+ statusSink
3516
+ });
3517
+ reportHandledCommandResult({
3518
+ client,
3519
+ eventId,
3520
+ status: "responded",
3521
+ code: "clawpool_exec_approval_command_handled",
3522
+ msg: "exec approval command handled",
3523
+ account,
3524
+ runtime: runtime2,
3525
+ statusSink
3526
+ });
3527
+ return;
3528
+ } catch (err) {
3529
+ const message = err instanceof Error ? err.message : String(err);
3530
+ runtime2.error(
3531
+ `[clawpool:${account.accountId}] exec approval command failed ${baseLogContext}: ${message}`
3532
+ );
3533
+ statusSink?.({ lastError: message });
3534
+ reportHandledCommandResult({
3535
+ client,
3536
+ eventId,
3537
+ status: "failed",
3538
+ code: "clawpool_exec_approval_command_failed",
3539
+ msg: message,
3540
+ account,
3541
+ runtime: runtime2,
3542
+ statusSink
3543
+ });
3544
+ throw err;
3545
+ }
3546
+ }
3547
+ const runAbortController = new AbortController();
3548
+ const activeRun = registerActiveReplyRun({
3549
+ accountId: account.accountId,
3550
+ eventId: eventId || `${sessionId}:${messageSid}`,
3551
+ sessionId,
3552
+ controller: runAbortController
3553
+ });
3554
+ runtime2.log(
3555
+ `[clawpool:${account.accountId}] active reply run registered eventId=${eventId || `${sessionId}:${messageSid}`} sessionId=${sessionId} messageSid=${messageSid} activeRun=${activeRun ? "true" : "false"}`
3556
+ );
3557
+ try {
3558
+ const route = core.channel.routing.resolveAgentRoute({
3559
+ cfg: config,
3560
+ channel: "clawpool",
3561
+ accountId: account.accountId,
3562
+ peer: {
3563
+ kind: isGroup ? "group" : "direct",
3564
+ id: sessionId
3565
+ }
3566
+ });
3567
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, {
3568
+ agentId: route.agentId
3569
+ });
3570
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
3571
+ storePath,
3572
+ sessionKey: route.sessionKey
3573
+ });
3574
+ const fromLabel = isGroup ? `group:${sessionId}/${senderId || "unknown"}` : `user:${senderId || "unknown"}`;
3575
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
3576
+ const body = core.channel.reply.formatAgentEnvelope({
3577
+ channel: "Clawpool",
3578
+ from: fromLabel,
3579
+ timestamp: createdAt,
3580
+ previousTimestamp,
3581
+ envelope: envelopeOptions,
3582
+ body: bodyForAgent
3583
+ });
3584
+ const from = isGroup ? `clawpool:group:${sessionId}:${senderId || "unknown"}` : `clawpool:${senderId || "unknown"}`;
3585
+ const to = `clawpool:${sessionId}`;
3586
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
3587
+ Body: body,
3588
+ BodyForAgent: bodyForAgent,
3589
+ RawBody: rawBody,
3590
+ CommandBody: rawBody,
3591
+ // Clawpool inbound text is end-user chat content; do not parse it as OpenClaw slash/bang commands.
3592
+ BodyForCommands: "",
3593
+ From: from,
3594
+ To: to,
3595
+ SessionKey: route.sessionKey,
3596
+ AccountId: route.accountId,
3597
+ ChatType: chatType,
3598
+ ConversationLabel: fromLabel,
3599
+ SenderName: senderId || void 0,
3600
+ SenderId: senderId || void 0,
3601
+ CommandAuthorized: false,
3602
+ Provider: "clawpool",
3603
+ Surface: "clawpool",
3604
+ MessageSid: messageSid,
3605
+ // This field carries the inbound quoted message id from end user (event.quoted_message_id).
3606
+ // It is not the outbound reply anchor used when plugin sends replies back to Aibot.
3607
+ ReplyToMessageSid: quotedMessageId,
3608
+ OriginatingChannel: "clawpool",
3609
+ OriginatingTo: to
3610
+ });
3611
+ const routeSessionKey = ctxPayload.SessionKey ?? route.sessionKey;
3612
+ await core.channel.session.recordInboundSession({
3613
+ storePath,
3614
+ sessionKey: routeSessionKey,
3615
+ ctx: ctxPayload,
3616
+ onRecordError: (err) => runtime2.error(`clawpool session meta update failed: ${String(err)}`)
3617
+ });
3618
+ await bindSessionRouteMapping({
3619
+ client,
3620
+ account,
3621
+ runtime: runtime2,
3622
+ sessionId,
3623
+ routeSessionKey,
3624
+ statusSink: statusSink ? (patch) => statusSink({ lastError: patch.lastError }) : void 0
3625
+ });
3626
+ if (eventId) {
3627
+ try {
3628
+ client.ackEvent(eventId, {
3629
+ sessionId,
3630
+ msgId: messageSid,
3631
+ receivedAt: Date.now()
3632
+ });
3633
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3634
+ confirmInboundEvent(inboundEvent.claim);
3635
+ inboundEventAccepted = true;
3636
+ } catch (err) {
3637
+ runtime2.error(`[clawpool:${account.accountId}] event ack failed eventId=${eventId}: ${String(err)}`);
3638
+ statusSink?.({ lastError: String(err) });
3639
+ }
3640
+ } else {
3641
+ confirmInboundEvent(inboundEvent.claim);
3642
+ inboundEventAccepted = true;
3643
+ }
3644
+ const outboundQuotedMessageId = normalizeNumericMessageId(event.msg_id);
3645
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
3646
+ cfg: config,
3647
+ agentId: route.agentId,
3648
+ channel: "clawpool",
3649
+ accountId: account.accountId
3650
+ });
3651
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
3652
+ cfg: config,
3653
+ channel: "clawpool",
3654
+ accountId: account.accountId
3655
+ });
3656
+ const streamClientMsgId = `reply_${messageSid}_stream`;
3657
+ const retryPolicy = resolveUpstreamRetryPolicy(account);
3658
+ let composingSet = false;
3659
+ let composingRenewTimer = null;
3660
+ let eventResultReported = false;
3661
+ let stopResultReported = false;
3662
+ const setComposing = (active) => {
3663
+ try {
3664
+ client.setSessionComposing(sessionId, active, {
3665
+ refEventId: eventId || void 0,
3666
+ refMsgId: outboundQuotedMessageId
3667
+ });
3668
+ composingSet = active;
3669
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3670
+ } catch (err) {
3671
+ runtime2.error(
3672
+ `[clawpool:${account.accountId}] session activity update failed eventId=${eventId || "-"} sessionId=${sessionId} active=${active}: ${String(err)}`
3673
+ );
3674
+ statusSink?.({ lastError: String(err) });
3675
+ }
3676
+ };
3677
+ const stopComposingRenewal = () => {
3678
+ if (composingRenewTimer) {
3679
+ clearTimeout(composingRenewTimer);
3680
+ composingRenewTimer = null;
3681
+ }
3682
+ };
3683
+ const scheduleComposingRenewal = () => {
3684
+ if (!composingSet || eventResultReported || visibleOutputSent || composingRenewTimer) {
3685
+ return;
3686
+ }
3687
+ composingRenewTimer = setTimeout(() => {
3688
+ composingRenewTimer = null;
3689
+ if (!composingSet || eventResultReported || visibleOutputSent) {
3690
+ return;
3691
+ }
3692
+ setComposing(true);
3693
+ scheduleComposingRenewal();
3694
+ }, composingRenewIntervalMs);
3695
+ };
3696
+ const reportEventResult = (status, code = "", msg = "") => {
3697
+ if (eventResultReported) {
3698
+ return;
3699
+ }
3700
+ eventResultReported = true;
3701
+ stopComposingRenewal();
3702
+ if (!eventId) {
3703
+ return;
3704
+ }
3705
+ try {
3706
+ client.sendEventResult({
3707
+ event_id: eventId,
3708
+ status,
3709
+ code: code || void 0,
3710
+ msg: msg || void 0,
3711
+ updated_at: Date.now()
3712
+ });
3713
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3714
+ } catch (err) {
3715
+ runtime2.error(
3716
+ `[clawpool:${account.accountId}] event result send failed eventId=${eventId} status=${status}: ${String(err)}`
3717
+ );
3718
+ statusSink?.({ lastError: String(err) });
3719
+ }
3720
+ };
3721
+ const reportStopResult = (status, code = "", msg = "") => {
3722
+ if (stopResultReported || !eventId || !activeRun?.stopRequested) {
3723
+ return;
3724
+ }
3725
+ stopResultReported = true;
3726
+ try {
3727
+ client.sendEventStopResult({
3728
+ stop_id: activeRun.stopId,
3729
+ event_id: eventId,
3730
+ status,
3731
+ code: code || void 0,
3732
+ msg: msg || void 0,
3733
+ updated_at: Date.now()
3734
+ });
3735
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3736
+ runtime2.log(
3737
+ `[clawpool:${account.accountId}] event_stop_result sent eventId=${eventId} stopId=${activeRun.stopId || "-"} status=${status} code=${code || "-"} msg=${msg || "-"}`
3738
+ );
3739
+ } catch (err) {
3740
+ runtime2.error(
3741
+ `[clawpool:${account.accountId}] event_stop_result send failed eventId=${eventId} status=${status}: ${String(err)}`
3742
+ );
3743
+ statusSink?.({ lastError: String(err) });
3744
+ }
3745
+ };
3746
+ const clearComposing = () => {
3747
+ stopComposingRenewal();
3748
+ if (composingSet) {
3749
+ setComposing(false);
3750
+ }
3751
+ };
3752
+ const markVisibleOutputSent = () => {
3753
+ if (visibleOutputSent) {
3754
+ return;
3755
+ }
3756
+ visibleOutputSent = true;
3757
+ clearComposing();
3758
+ reportEventResult("responded");
3759
+ };
3760
+ setComposing(true);
3761
+ scheduleComposingRenewal();
3762
+ try {
3763
+ for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
3764
+ let hasSentBlock = false;
3765
+ let outboundCounter = 0;
3766
+ let attemptHasOutbound = false;
3767
+ let retryGuardedText = null;
3768
+ const attemptLabel = `${attempt}/${retryPolicy.maxAttempts}`;
3769
+ const finishStreamIfNeeded = async () => {
3770
+ if (!hasSentBlock) {
3771
+ return;
3772
+ }
3773
+ if (runAbortController.signal.aborted) {
3774
+ runtime2.log(
3775
+ `[clawpool:${account.accountId}] skip stream finish due to abort ${buildEventLogContext({
3776
+ eventId,
3777
+ sessionId,
3778
+ messageSid,
3779
+ clientMsgId: streamClientMsgId
3780
+ })} abortReason=${resolveAbortReason(runAbortController.signal)}`
3781
+ );
3782
+ hasSentBlock = false;
3783
+ return;
3784
+ }
3785
+ hasSentBlock = false;
3786
+ try {
3787
+ const finishContext = buildEventLogContext({
3788
+ eventId,
3789
+ sessionId,
3790
+ messageSid,
3791
+ clientMsgId: streamClientMsgId
3792
+ });
3793
+ const finishDelayMs = resolveStreamFinishDelayMs(account);
3794
+ if (finishDelayMs > 0) {
3795
+ runtime2.log(
3796
+ `[clawpool:${account.accountId}] stream finish delay ${finishContext} delayMs=${finishDelayMs}`
3797
+ );
3798
+ await sleep2(finishDelayMs);
3799
+ }
3800
+ runtime2.log(
3801
+ `[clawpool:${account.accountId}] stream finish ${finishContext}`
3802
+ );
3803
+ await client.sendStreamChunk(sessionId, "", {
3804
+ eventId,
3805
+ clientMsgId: streamClientMsgId,
3806
+ quotedMessageId: outboundQuotedMessageId,
3807
+ isFinish: true
3808
+ });
3809
+ attemptHasOutbound = true;
3810
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
3811
+ } catch (err) {
3812
+ runtime2.error(`[clawpool:${account.accountId}] stream finish failed: ${String(err)}`);
3813
+ statusSink?.({ lastError: String(err) });
3814
+ }
3815
+ };
3816
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
3817
+ ctx: ctxPayload,
3818
+ cfg: config,
3819
+ dispatcherOptions: {
3820
+ ...prefixOptions,
3821
+ deliver: async (payload, info) => {
3822
+ outboundCounter++;
3823
+ const outPayload = payload;
3824
+ const guardedText = guardInternalReplyText(String(outPayload.text ?? ""));
3825
+ const normalizedPayload = guardedText ? { ...outPayload, text: guardedText.userText } : outPayload;
3826
+ const hasMedia = Boolean(normalizedPayload.mediaUrl) || (normalizedPayload.mediaUrls?.length ?? 0) > 0;
3827
+ const text = core.channel.text.convertMarkdownTables(normalizedPayload.text ?? "", tableMode);
3828
+ const streamedTextAlreadyVisible = hasSentBlock;
3829
+ const deliverContext = buildEventLogContext({
3830
+ eventId,
3831
+ sessionId,
3832
+ messageSid,
3833
+ clientMsgId: info.kind === "block" ? streamClientMsgId : `reply_${messageSid}_${outboundCounter}`,
3834
+ outboundCounter
3835
+ });
3836
+ runtime2.log(
3837
+ `[clawpool:${account.accountId}] deliver ${deliverContext} kind=${info.kind} textLen=${text.length} hasMedia=${hasMedia} streamedBefore=${streamedTextAlreadyVisible}`
3838
+ );
3839
+ if (guardedText) {
3840
+ runtime2.error(
3841
+ `[clawpool:${account.accountId}] rewrite internal reply text ${deliverContext} code=${guardedText.code} raw=${JSON.stringify(guardedText.rawText)}`
3842
+ );
3843
+ }
3844
+ if (guardedText && retryGuardedText == null && isRetryableGuardedReply(guardedText) && !attemptHasOutbound && !hasSentBlock) {
3845
+ retryGuardedText = guardedText;
3846
+ runtime2.log(
3847
+ `[clawpool:${account.accountId}] defer guarded upstream reply for retry ${deliverContext} attempt=${attemptLabel} code=${guardedText.code}`
3848
+ );
3849
+ return;
3850
+ }
3851
+ if (retryGuardedText && !attemptHasOutbound && !hasSentBlock) {
3852
+ runtime2.log(
3853
+ `[clawpool:${account.accountId}] skip outbound while retry pending ${deliverContext} attempt=${attemptLabel} code=${retryGuardedText.code}`
3854
+ );
3855
+ return;
3856
+ }
3857
+ if (info.kind === "block" && !guardedText && !hasMedia && text) {
3858
+ const didSendBlock = await deliverAibotStreamBlock({
3859
+ text,
3860
+ client,
3861
+ account,
3862
+ sessionId,
3863
+ abortSignal: runAbortController.signal,
3864
+ eventId,
3865
+ messageSid,
3866
+ quotedMessageId: outboundQuotedMessageId,
3867
+ clientMsgId: streamClientMsgId,
3868
+ runtime: runtime2,
3869
+ statusSink
3870
+ });
3871
+ hasSentBlock = hasSentBlock || didSendBlock;
3872
+ attemptHasOutbound = attemptHasOutbound || didSendBlock;
3873
+ if (didSendBlock) {
3874
+ markVisibleOutputSent();
3875
+ }
3876
+ return;
3877
+ }
3878
+ await finishStreamIfNeeded();
3879
+ if (info.kind === "final" && streamedTextAlreadyVisible && !hasMedia && text) {
3880
+ runtime2.log(
3881
+ `[clawpool:${account.accountId}] skip final text after streamed block ${deliverContext} textLen=${text.length}`
3882
+ );
3883
+ return;
3884
+ }
3885
+ const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
3886
+ runtime2.log(
3887
+ `[clawpool:${account.accountId}] deliver message ${buildEventLogContext({
3888
+ eventId,
3889
+ sessionId,
3890
+ messageSid,
3891
+ clientMsgId: stableClientMsgId,
3892
+ outboundCounter
3893
+ })} textLen=${text.length} hasMedia=${hasMedia}`
3894
+ );
3895
+ const didSendMessage = await deliverAibotMessage({
3896
+ payload: normalizedPayload,
3897
+ client,
3898
+ account,
3899
+ sessionId,
3900
+ abortSignal: runAbortController.signal,
3901
+ eventId,
3902
+ quotedMessageId: outboundQuotedMessageId,
3903
+ runtime: runtime2,
3904
+ statusSink,
3905
+ stableClientMsgId,
3906
+ tableMode
3907
+ });
3908
+ attemptHasOutbound = attemptHasOutbound || didSendMessage;
3909
+ if (didSendMessage) {
3910
+ markVisibleOutputSent();
3911
+ }
3912
+ },
3913
+ onError: (err, info) => {
3914
+ runtime2.error(`[clawpool:${account.accountId}] ${info.kind} reply failed: ${String(err)}`);
3915
+ statusSink?.({ lastError: String(err) });
3916
+ }
3917
+ },
3918
+ replyOptions: {
3919
+ abortSignal: runAbortController.signal,
3920
+ onModelSelected
3921
+ }
3922
+ });
3923
+ runtime2.log(
3924
+ `[clawpool:${account.accountId}] dispatch complete ${baseLogContext} attempt=${attemptLabel} queuedFinal=${dispatchResult.queuedFinal} counts=${JSON.stringify(dispatchResult.counts)}`
3925
+ );
3926
+ await finishStreamIfNeeded();
3927
+ if (!visibleOutputSent && consumeSilentUnsendCompleted(messageSid)) {
3928
+ runtime2.log(
3929
+ `[clawpool:${account.accountId}] silent unsend completed ${baseLogContext} attempt=${attemptLabel}`
3930
+ );
3931
+ reportEventResult("responded");
3932
+ }
3933
+ if (!visibleOutputSent && shouldTreatDispatchAsRespondedWithoutVisibleOutput(dispatchResult)) {
3934
+ runtime2.log(
3935
+ `[clawpool:${account.accountId}] dispatch completed without visible reply but produced actionable outcome ${baseLogContext} attempt=${attemptLabel}`
3936
+ );
3937
+ reportEventResult("responded");
3938
+ }
3939
+ if (retryGuardedText && !attemptHasOutbound) {
3940
+ if (attempt < retryPolicy.maxAttempts) {
3941
+ const delayMs = resolveUpstreamRetryDelayMs(retryPolicy, attempt);
3942
+ runtime2.error(
3943
+ `[clawpool:${account.accountId}] upstream guarded reply retry ${baseLogContext} code=${retryGuardedText.code} attempt=${attemptLabel} next=${attempt + 1}/${retryPolicy.maxAttempts} delayMs=${delayMs}`
3944
+ );
3945
+ if (delayMs > 0) {
3946
+ await sleep2(delayMs);
3947
+ }
3948
+ continue;
3949
+ }
3950
+ outboundCounter++;
3951
+ const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
3952
+ runtime2.error(
3953
+ `[clawpool:${account.accountId}] upstream guarded reply retry exhausted ${baseLogContext} code=${retryGuardedText.code} attempts=${retryPolicy.maxAttempts}`
3954
+ );
3955
+ const didSendMessage = await deliverAibotMessage({
3956
+ payload: {
3957
+ text: retryGuardedText.userText
3958
+ },
3959
+ client,
3960
+ account,
3961
+ sessionId,
3962
+ abortSignal: runAbortController.signal,
3963
+ eventId,
3964
+ quotedMessageId: outboundQuotedMessageId,
3965
+ runtime: runtime2,
3966
+ statusSink,
3967
+ stableClientMsgId,
3968
+ tableMode
3969
+ });
3970
+ attemptHasOutbound = attemptHasOutbound || didSendMessage;
3971
+ if (didSendMessage) {
3972
+ markVisibleOutputSent();
3973
+ }
3974
+ }
3975
+ break;
3976
+ }
3977
+ if (!visibleOutputSent && !eventResultReported) {
3978
+ reportEventResult("failed", "clawpool_no_outbound_reply", "no outbound reply emitted");
3979
+ }
3980
+ } catch (err) {
3981
+ if (runAbortController.signal.aborted) {
3982
+ runtime2.log(
3983
+ `[clawpool:${account.accountId}] dispatch aborted ${baseLogContext} stopRequested=${activeRun?.stopRequested === true} abortReason=${resolveAbortReason(runAbortController.signal)}`
3984
+ );
3985
+ clearComposing();
3986
+ if (activeRun?.stopRequested) {
3987
+ if (!visibleOutputSent) {
3988
+ reportEventResult("canceled", "owner_requested_stop", "owner requested stop");
3989
+ }
3990
+ reportStopResult("stopped", "owner_requested_stop", "owner requested stop");
3991
+ return;
3992
+ }
3993
+ }
3994
+ if (!visibleOutputSent) {
3995
+ const message = err instanceof Error ? err.message : String(err);
3996
+ reportEventResult("failed", "clawpool_dispatch_failed", message);
3997
+ }
3998
+ reportStopResult("failed", "clawpool_stop_failed", err instanceof Error ? err.message : String(err));
3999
+ throw err;
4000
+ } finally {
4001
+ stopComposingRenewal();
4002
+ if (composingSet) {
4003
+ setComposing(false);
4004
+ }
4005
+ }
4006
+ } finally {
4007
+ runtime2.log(
4008
+ `[clawpool:${account.accountId}] active reply run clearing eventId=${activeRun?.eventId || "-"} sessionId=${activeRun?.sessionId || sessionId} stopRequested=${activeRun?.stopRequested === true} abortReason=${activeRun ? resolveAbortReason(activeRun.controller.signal) : "-"} visibleOutputSent=${visibleOutputSent}`
4009
+ );
4010
+ clearActiveReplyRun(activeRun);
4011
+ if (!inboundEventAccepted) {
4012
+ releaseInboundEvent(inboundEvent.claim);
4013
+ }
4014
+ }
4015
+ }
4016
+ async function monitorAibotProvider(options) {
4017
+ const { account, config, runtime: runtime2, abortSignal, statusSink } = options;
4018
+ let client;
4019
+ const guardedStatusSink = (patch) => {
4020
+ if (!isActiveMonitor(account.accountId, client)) {
4021
+ return;
4022
+ }
4023
+ statusSink?.(patch);
4024
+ };
4025
+ client = new AibotWsClient(account, {
4026
+ logger: {
4027
+ info: (message) => runtime2.log(message),
4028
+ warn: (message) => runtime2.log(`[warn] ${message}`),
4029
+ error: (message) => runtime2.error(message),
4030
+ debug: (message) => runtime2.log(message)
4031
+ },
4032
+ onStatus: (status) => {
4033
+ guardedStatusSink({
4034
+ running: status.running,
4035
+ connected: status.connected,
4036
+ lastError: status.lastError,
4037
+ lastConnectAt: status.lastConnectAt ?? void 0,
4038
+ lastDisconnectAt: status.lastDisconnectAt ?? void 0
4039
+ });
4040
+ },
4041
+ onEventMsg: (event) => {
4042
+ if (!isActiveMonitor(account.accountId, client)) {
4043
+ return;
4044
+ }
4045
+ guardedStatusSink({ lastInboundAt: Date.now() });
4046
+ void processEvent({
4047
+ event,
4048
+ account,
4049
+ config,
4050
+ runtime: runtime2,
4051
+ client,
4052
+ statusSink: guardedStatusSink
4053
+ }).catch((err) => {
4054
+ if (!isActiveMonitor(account.accountId, client)) {
4055
+ return;
4056
+ }
4057
+ const msg = err instanceof Error ? err.message : String(err);
4058
+ runtime2.error(`[clawpool:${account.accountId}] process event failed: ${msg}`);
4059
+ guardedStatusSink({ lastError: msg });
4060
+ });
4061
+ },
4062
+ onEventRevoke: (event) => {
4063
+ if (!isActiveMonitor(account.accountId, client)) {
4064
+ return;
4065
+ }
4066
+ guardedStatusSink({ lastInboundAt: Date.now() });
4067
+ try {
4068
+ const eventId = String(event.event_id ?? "").trim();
4069
+ if (eventId) {
4070
+ client.ackEvent(eventId, {
4071
+ sessionId: event.session_id,
4072
+ msgId: event.msg_id
4073
+ });
4074
+ }
4075
+ const revokeEvent = enqueueRevokeSystemEvent({
4076
+ core: getAibotRuntime(),
4077
+ event,
4078
+ account,
4079
+ config
4080
+ });
4081
+ runtime2.log(
4082
+ `[clawpool:${account.accountId}] inbound revoke sessionId=${revokeEvent.sessionId} messageSid=${revokeEvent.messageId} routeSessionKey=${revokeEvent.sessionKey}`
4083
+ );
4084
+ } catch (err) {
4085
+ const msg = err instanceof Error ? err.message : String(err);
4086
+ runtime2.error(`[clawpool:${account.accountId}] process revoke event failed: ${msg}`);
4087
+ guardedStatusSink({ lastError: msg });
4088
+ }
4089
+ },
4090
+ onEventStop: (payload) => {
4091
+ if (!isActiveMonitor(account.accountId, client)) {
4092
+ return;
4093
+ }
4094
+ guardedStatusSink({ lastInboundAt: Date.now() });
4095
+ handleEventStop({
4096
+ payload,
4097
+ account,
4098
+ runtime: runtime2,
4099
+ client,
4100
+ statusSink: guardedStatusSink
4101
+ });
4102
+ }
4103
+ });
4104
+ const previousClient = registerActiveMonitor(account.accountId, client);
4105
+ if (previousClient) {
4106
+ runtime2.log(`[clawpool:${account.accountId}] stopping superseded clawpool monitor before restart`);
4107
+ previousClient.stop();
4108
+ }
4109
+ setActiveAibotClient(account.accountId, client);
4110
+ try {
4111
+ await client.start(abortSignal);
4112
+ } catch (err) {
4113
+ clearActiveAibotClient(account.accountId, client);
4114
+ clearActiveMonitor(account.accountId, client);
4115
+ throw err;
4116
+ }
4117
+ void client.waitUntilStopped().catch((err) => {
4118
+ if (!isActiveMonitor(account.accountId, client)) {
4119
+ return;
4120
+ }
4121
+ const msg = err instanceof Error ? err.message : String(err);
4122
+ runtime2.error(`[clawpool:${account.accountId}] background run loop failed: ${msg}`);
4123
+ guardedStatusSink({ lastError: msg });
4124
+ }).finally(() => {
4125
+ clearActiveAibotClient(account.accountId, client);
4126
+ clearActiveMonitor(account.accountId, client);
4127
+ });
4128
+ return {
4129
+ stop: () => {
4130
+ clearActiveAibotClient(account.accountId, client);
4131
+ clearActiveMonitor(account.accountId, client);
4132
+ client.stop();
4133
+ }
4134
+ };
4135
+ }
4136
+
4137
+ // src/setup-config.ts
4138
+ import {
4139
+ applyAccountNameToChannelSection,
4140
+ migrateBaseNameToDefaultAccount
4141
+ } from "openclaw/plugin-sdk/core";
4142
+ function resolveSetupValues(input) {
4143
+ const apiKey = String(input.token ?? input.appToken ?? "").trim();
4144
+ const wsUrl = String(input.httpUrl ?? input.webhookUrl ?? input.url ?? "").trim();
4145
+ const agentId = String(input.userId ?? "").trim();
4146
+ return {
4147
+ apiKey: apiKey || void 0,
4148
+ wsUrl: wsUrl || void 0,
4149
+ agentId: agentId || void 0
4150
+ };
4151
+ }
4152
+ function applySetupAccountConfig(params) {
4153
+ const { cfg, accountId, name, values } = params;
4154
+ const namedConfig = applyAccountNameToChannelSection({
4155
+ cfg,
4156
+ channelKey: "clawpool",
4157
+ accountId,
4158
+ name
4159
+ });
4160
+ const next = accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
4161
+ cfg: namedConfig,
4162
+ channelKey: "clawpool"
4163
+ }) : namedConfig;
4164
+ if (accountId === DEFAULT_ACCOUNT_ID) {
4165
+ return {
4166
+ ...next,
4167
+ channels: {
4168
+ ...next.channels,
4169
+ clawpool: {
4170
+ ...next.channels?.clawpool,
4171
+ enabled: true,
4172
+ ...values.apiKey ? { apiKey: values.apiKey } : {},
4173
+ ...values.wsUrl ? { wsUrl: values.wsUrl } : {},
4174
+ ...values.agentId ? { agentId: values.agentId } : {}
4175
+ }
4176
+ }
4177
+ };
4178
+ }
4179
+ return {
4180
+ ...next,
4181
+ channels: {
4182
+ ...next.channels,
4183
+ clawpool: {
4184
+ ...next.channels?.clawpool,
4185
+ enabled: true,
4186
+ accounts: {
4187
+ ...next.channels?.clawpool?.accounts ?? {},
4188
+ [accountId]: {
4189
+ ...next.channels?.clawpool?.accounts?.[accountId],
4190
+ enabled: true,
4191
+ ...values.apiKey ? { apiKey: values.apiKey } : {},
4192
+ ...values.wsUrl ? { wsUrl: values.wsUrl } : {},
4193
+ ...values.agentId ? { agentId: values.agentId } : {}
4194
+ }
4195
+ }
4196
+ }
4197
+ }
4198
+ };
4199
+ }
4200
+
4201
+ // src/channel.ts
4202
+ var meta = {
4203
+ id: "clawpool",
4204
+ label: "Clawpool",
4205
+ selectionLabel: "Clawpool",
4206
+ docsPath: "/channels/clawpool",
4207
+ blurb: "Bridge OpenClaw to Clawpool over the ClawPool Agent API WebSocket.",
4208
+ aliases: ["cp", "clowpool"],
4209
+ order: 90
4210
+ };
4211
+ function normalizeQuotedMessageId(rawInput) {
4212
+ const raw = String(rawInput ?? "").trim();
4213
+ if (!raw) {
4214
+ return void 0;
4215
+ }
4216
+ if (/^\d+$/.test(raw)) {
4217
+ return raw;
4218
+ }
4219
+ const parsed = raw.split(":").at(-1)?.trim() ?? "";
4220
+ if (/^\d+$/.test(parsed)) {
4221
+ return parsed;
4222
+ }
4223
+ return void 0;
4224
+ }
4225
+ function logAibotOutboundAdapter(message) {
4226
+ console.info(`[clawpool:outbound] ${message}`);
4227
+ }
4228
+ function asAibotChannelConfig(cfg) {
4229
+ return cfg.channels?.clawpool ?? {};
4230
+ }
4231
+ function buildAccountSnapshot(params) {
4232
+ const { account, runtime: runtime2 } = params;
4233
+ return {
4234
+ accountId: account.accountId,
4235
+ name: account.name,
4236
+ enabled: account.enabled,
4237
+ configured: account.configured,
4238
+ running: runtime2?.running ?? false,
4239
+ connected: runtime2?.connected ?? false,
4240
+ lastError: runtime2?.lastError ?? null,
4241
+ lastStartAt: runtime2?.lastStartAt ?? null,
4242
+ lastStopAt: runtime2?.lastStopAt ?? null,
4243
+ lastInboundAt: runtime2?.lastInboundAt ?? null,
4244
+ lastOutboundAt: runtime2?.lastOutboundAt ?? null,
4245
+ dmPolicy: account.config.dmPolicy ?? "open",
4246
+ tokenSource: account.apiKey ? "config" : "none"
4247
+ };
4248
+ }
4249
+ var AibotConfigSchema = {
4250
+ type: "object",
4251
+ additionalProperties: true,
4252
+ properties: {}
4253
+ };
4254
+ function chunkTextForOutbound(text, limit) {
4255
+ if (!text) return [];
4256
+ if (limit <= 0 || text.length <= limit) return [text];
4257
+ const chunks = [];
4258
+ let remaining = text;
4259
+ while (remaining.length > limit) {
4260
+ const window = remaining.slice(0, limit);
4261
+ const lastNewline = window.lastIndexOf("\n");
4262
+ const lastSpace = window.lastIndexOf(" ");
4263
+ const candidateBreak = lastNewline > 0 ? lastNewline : lastSpace;
4264
+ const breakIdx = Number.isFinite(candidateBreak) && candidateBreak > 0 && candidateBreak <= limit ? candidateBreak : limit;
4265
+ const chunk = remaining.slice(0, breakIdx).trimEnd();
4266
+ if (chunk.length > 0) chunks.push(chunk);
4267
+ const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx] ?? "");
4268
+ const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
4269
+ remaining = remaining.slice(nextStart).trimStart();
4270
+ }
4271
+ if (remaining.length) chunks.push(remaining);
4272
+ return chunks;
4273
+ }
4274
+ var aibotPlugin = {
4275
+ id: "clawpool",
4276
+ meta,
4277
+ capabilities: {
4278
+ chatTypes: ["direct", "group"],
4279
+ media: true,
4280
+ reactions: true,
4281
+ unsend: true,
4282
+ threads: false,
4283
+ polls: false,
4284
+ nativeCommands: false,
4285
+ blockStreaming: true
4286
+ },
4287
+ actions: aibotMessageActions,
4288
+ reload: {
4289
+ configPrefixes: ["channels.clawpool"]
4290
+ },
4291
+ configSchema: {
4292
+ schema: AibotConfigSchema
4293
+ },
4294
+ config: {
4295
+ listAccountIds: (cfg) => listAibotAccountIds(cfg),
4296
+ resolveAccount: (cfg, accountId) => resolveAibotAccount({ cfg, accountId }),
4297
+ defaultAccountId: (cfg) => resolveDefaultAibotAccountId(cfg),
4298
+ setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
4299
+ cfg,
4300
+ sectionKey: "clawpool",
4301
+ accountId,
4302
+ enabled,
4303
+ allowTopLevel: true
4304
+ }),
4305
+ deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
4306
+ cfg,
4307
+ sectionKey: "clawpool",
4308
+ accountId,
4309
+ clearBaseFields: [
4310
+ "name",
4311
+ "wsUrl",
4312
+ "agentId",
4313
+ "apiKey",
4314
+ "reconnectMs",
4315
+ "reconnectMaxMs",
4316
+ "reconnectStableMs",
4317
+ "connectTimeoutMs",
4318
+ "keepalivePingMs",
4319
+ "keepaliveTimeoutMs",
4320
+ "upstreamRetryMaxAttempts",
4321
+ "upstreamRetryBaseDelayMs",
4322
+ "upstreamRetryMaxDelayMs",
4323
+ "maxChunkChars",
4324
+ "execApprovals",
4325
+ "dmPolicy",
4326
+ "allowFrom",
4327
+ "defaultTo"
4328
+ ]
4329
+ }),
4330
+ isConfigured: (account) => account.configured,
4331
+ describeAccount: (account, cfg) => {
4332
+ const root = asAibotChannelConfig(cfg);
4333
+ return {
4334
+ accountId: account.accountId,
4335
+ name: account.name,
4336
+ enabled: account.enabled,
4337
+ configured: account.configured,
4338
+ running: false,
4339
+ connected: false,
4340
+ lastError: account.configured ? null : "missing wsUrl/agentId/apiKey (or CLAWPOOL_WS_URL/CLAWPOOL_AGENT_ID/CLAWPOOL_API_KEY)",
4341
+ dmPolicy: account.config.dmPolicy ?? "open",
4342
+ tokenSource: account.apiKey ? "config" : "none",
4343
+ mode: "block_streaming",
4344
+ baseUrl: redactAibotWsUrl(account.wsUrl),
4345
+ allowFrom: account.config.allowFrom?.map((entry) => String(entry).trim()).filter(Boolean) ?? [],
4346
+ nameSource: root.accounts?.[account.accountId]?.name ? "account" : "base"
4347
+ };
4348
+ },
4349
+ resolveAllowFrom: ({ cfg, accountId }) => resolveAibotAccount({ cfg, accountId }).config.allowFrom?.map((entry) => String(entry)) ?? [],
4350
+ formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
4351
+ resolveDefaultTo: ({ cfg, accountId }) => resolveAibotAccount({ cfg, accountId }).config.defaultTo?.trim() || void 0
4352
+ },
4353
+ setup: {
4354
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
4355
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection2({
4356
+ cfg,
4357
+ channelKey: "clawpool",
4358
+ accountId,
4359
+ name
4360
+ }),
4361
+ validateInput: ({ input }) => {
4362
+ const values = resolveSetupValues(input);
4363
+ const hasAny = Boolean(values.apiKey || values.wsUrl || values.agentId);
4364
+ if (!hasAny) {
4365
+ return "clawpool setup requires at least one of: --token(api key), --http-url(ws url), --user-id(agent id)";
4366
+ }
4367
+ return null;
4368
+ },
4369
+ applyAccountConfig: ({ cfg, accountId, input }) => {
4370
+ const values = resolveSetupValues(input);
4371
+ return applySetupAccountConfig({
4372
+ cfg,
4373
+ accountId,
4374
+ name: input.name,
4375
+ values
4376
+ });
4377
+ }
4378
+ },
4379
+ security: {
4380
+ resolveDmPolicy: ({ cfg, accountId, account }) => {
4381
+ const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
4382
+ const isAccountScoped = Boolean(cfg.channels?.clawpool?.accounts?.[resolvedAccountId]);
4383
+ const basePath = isAccountScoped ? `channels.clawpool.accounts.${resolvedAccountId}.` : "channels.clawpool.";
4384
+ return {
4385
+ policy: account.config.dmPolicy ?? "open",
4386
+ allowFrom: account.config.allowFrom ?? [],
4387
+ policyPath: `${basePath}dmPolicy`,
4388
+ allowFromPath: basePath,
4389
+ approveHint: formatPairingApproveHint("clawpool")
4390
+ };
4391
+ }
4392
+ },
4393
+ messaging: {
4394
+ normalizeTarget: (raw) => normalizeAibotSessionTarget(raw),
4395
+ targetResolver: {
4396
+ looksLikeId: (raw) => Boolean(normalizeAibotSessionTarget(raw)),
4397
+ hint: "<session_id|route.sessionKey>"
4398
+ }
4399
+ },
4400
+ execApprovals: clawpoolExecApprovalAdapter,
4401
+ threading: {
4402
+ buildToolContext: ({ context, hasRepliedRef }) => ({
4403
+ currentChannelId: context.To?.trim() || void 0,
4404
+ currentMessageId: context.CurrentMessageId != null ? String(context.CurrentMessageId) : void 0,
4405
+ hasRepliedRef
4406
+ })
4407
+ },
4408
+ agentPrompt: {
4409
+ messageToolHints: () => [
4410
+ "- Clawpool `action=unsend` is a silent cleanup action: unsend the target `messageId`, unsend the recall command message when applicable, then end with `NO_REPLY` and do not send any confirmation text. Omit `sessionId`/`to` only when targeting the current Clawpool chat."
4411
+ ]
4412
+ },
4413
+ outbound: {
4414
+ deliveryMode: "direct",
4415
+ chunker: chunkTextForOutbound,
4416
+ chunkerMode: "markdown",
4417
+ textChunkLimit: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT,
4418
+ sendText: async ({ cfg, to, text, accountId, replyToId }) => {
4419
+ const account = resolveAibotAccount({ cfg, accountId });
4420
+ const client = requireActiveAibotClient(account.accountId);
4421
+ const rawTarget = String(to ?? "").trim() || "-";
4422
+ logAibotOutboundAdapter(
4423
+ `sendText target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
4424
+ );
4425
+ let resolvedTarget;
4426
+ try {
4427
+ resolvedTarget = await resolveAibotOutboundTarget({
4428
+ client,
4429
+ accountId: account.accountId,
4430
+ to
4431
+ });
4432
+ } catch (err) {
4433
+ logAibotOutboundAdapter(
4434
+ `sendText target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
4435
+ );
4436
+ throw err;
4437
+ }
4438
+ const sessionId = resolvedTarget.sessionId;
4439
+ const quotedMessageId = normalizeQuotedMessageId(replyToId);
4440
+ logAibotOutboundAdapter(
4441
+ `sendText accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${text.length} quotedMessageId=${quotedMessageId ?? "-"}`
4442
+ );
4443
+ const ack = await client.sendText(sessionId, text, {
4444
+ quotedMessageId
4445
+ });
4446
+ logAibotOutboundAdapter(
4447
+ `sendText ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
4448
+ );
4449
+ return {
4450
+ channel: "clawpool",
4451
+ messageId: String(ack.msg_id ?? ack.client_msg_id ?? Date.now())
4452
+ };
4453
+ },
4454
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => {
4455
+ const account = resolveAibotAccount({ cfg, accountId });
4456
+ const client = requireActiveAibotClient(account.accountId);
4457
+ const rawTarget = String(to ?? "").trim() || "-";
4458
+ logAibotOutboundAdapter(
4459
+ `sendMedia target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
4460
+ );
4461
+ let resolvedTarget;
4462
+ try {
4463
+ resolvedTarget = await resolveAibotOutboundTarget({
4464
+ client,
4465
+ accountId: account.accountId,
4466
+ to
4467
+ });
4468
+ } catch (err) {
4469
+ logAibotOutboundAdapter(
4470
+ `sendMedia target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
4471
+ );
4472
+ throw err;
4473
+ }
4474
+ const sessionId = resolvedTarget.sessionId;
4475
+ if (!mediaUrl) {
4476
+ throw new Error("clawpool sendMedia requires mediaUrl");
4477
+ }
4478
+ const quotedMessageId = normalizeQuotedMessageId(replyToId);
4479
+ logAibotOutboundAdapter(
4480
+ `sendMedia accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${(text ?? "").length} quotedMessageId=${quotedMessageId ?? "-"} mediaUrl=${mediaUrl}`
4481
+ );
4482
+ const ack = await client.sendMedia(sessionId, mediaUrl, text ?? "", {
4483
+ quotedMessageId
4484
+ });
4485
+ logAibotOutboundAdapter(
4486
+ `sendMedia ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
4487
+ );
4488
+ return {
4489
+ channel: "clawpool",
4490
+ messageId: String(ack.msg_id ?? ack.client_msg_id ?? Date.now())
4491
+ };
4492
+ },
4493
+ sendPayload: async ({ cfg, to, payload, accountId, replyToId }) => {
4494
+ const account = resolveAibotAccount({ cfg, accountId });
4495
+ const client = requireActiveAibotClient(account.accountId);
4496
+ const rawTarget = String(to ?? "").trim() || "-";
4497
+ logAibotOutboundAdapter(
4498
+ `sendPayload target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
4499
+ );
4500
+ let resolvedTarget;
4501
+ try {
4502
+ resolvedTarget = await resolveAibotOutboundTarget({
4503
+ client,
4504
+ accountId: account.accountId,
4505
+ to
4506
+ });
4507
+ } catch (err) {
4508
+ logAibotOutboundAdapter(
4509
+ `sendPayload target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
4510
+ );
4511
+ throw err;
4512
+ }
4513
+ const sessionId = resolvedTarget.sessionId;
4514
+ const quotedMessageId = normalizeQuotedMessageId(replyToId);
4515
+ const envelope = buildAibotOutboundEnvelope(payload);
4516
+ logAibotOutboundAdapter(
4517
+ `sendPayload accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${envelope.text.length} quotedMessageId=${quotedMessageId ?? "-"} cardKind=${envelope.cardKind ?? "none"}`
4518
+ );
4519
+ const delivery = await deliverAibotPayload({
4520
+ payload,
4521
+ text: envelope.text,
4522
+ extra: envelope.extra,
4523
+ client,
4524
+ account,
4525
+ sessionId,
4526
+ quotedMessageId
4527
+ });
4528
+ if (!delivery.sent) {
4529
+ throw new Error("clawpool sendPayload produced no visible delivery");
4530
+ }
4531
+ const messageId = delivery.firstMessageId ?? `clawpool_payload_${Date.now()}`;
4532
+ logAibotOutboundAdapter(
4533
+ `sendPayload ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${messageId} cardKind=${envelope.cardKind ?? "none"}`
4534
+ );
4535
+ return {
4536
+ channel: "clawpool",
4537
+ messageId
4538
+ };
4539
+ }
4540
+ },
4541
+ status: {
4542
+ defaultRuntime: {
4543
+ accountId: DEFAULT_ACCOUNT_ID,
4544
+ running: false,
4545
+ connected: false,
4546
+ lastError: null,
4547
+ lastStartAt: null,
4548
+ lastStopAt: null,
4549
+ lastInboundAt: null,
4550
+ lastOutboundAt: null
4551
+ },
4552
+ buildChannelSummary: ({ snapshot }) => ({
4553
+ configured: snapshot.configured ?? false,
4554
+ running: snapshot.running ?? false,
4555
+ connected: snapshot.connected ?? false,
4556
+ lastError: snapshot.lastError ?? null,
4557
+ lastInboundAt: snapshot.lastInboundAt ?? null,
4558
+ lastOutboundAt: snapshot.lastOutboundAt ?? null
4559
+ }),
4560
+ buildAccountSnapshot: ({ account, runtime: runtime2 }) => buildAccountSnapshot({ account, runtime: runtime2 }),
4561
+ collectStatusIssues: (accounts) => accounts.flatMap((account) => {
4562
+ if (!account.enabled) {
4563
+ return [];
4564
+ }
4565
+ if (!account.configured) {
4566
+ return [
4567
+ {
4568
+ channel: "clawpool",
4569
+ accountId: account.accountId,
4570
+ kind: "config",
4571
+ message: "Clawpool account is not configured. Set wsUrl/agentId/apiKey (or CLAWPOOL_WS_URL/CLAWPOOL_AGENT_ID/CLAWPOOL_API_KEY)."
4572
+ }
4573
+ ];
4574
+ }
4575
+ if (account.running && !account.connected) {
4576
+ return [
4577
+ {
4578
+ channel: "clawpool",
4579
+ accountId: account.accountId,
4580
+ kind: "runtime",
4581
+ message: "Clawpool channel is running but not connected."
4582
+ }
4583
+ ];
4584
+ }
4585
+ if (typeof account.lastError === "string" && account.lastError.trim()) {
4586
+ return [
4587
+ {
4588
+ channel: "clawpool",
4589
+ accountId: account.accountId,
4590
+ kind: "runtime",
4591
+ message: account.lastError
4592
+ }
4593
+ ];
4594
+ }
4595
+ return [];
4596
+ })
4597
+ },
4598
+ gateway: {
4599
+ startAccount: async (ctx) => {
4600
+ const account = ctx.account;
4601
+ if (!account.configured) {
4602
+ throw new Error(
4603
+ `clawpool account "${account.accountId}" not configured: require wsUrl + agentId + apiKey`
4604
+ );
4605
+ }
4606
+ ctx.log?.info?.(
4607
+ `[${account.accountId}] starting clawpool monitor (${redactAibotWsUrl(account.wsUrl)})`
4608
+ );
4609
+ ctx.setStatus({
4610
+ ...ctx.getStatus(),
4611
+ running: true,
4612
+ connected: false,
4613
+ lastError: null,
4614
+ lastStartAt: Date.now()
4615
+ });
4616
+ const monitor = await monitorAibotProvider({
4617
+ account,
4618
+ config: ctx.cfg,
4619
+ runtime: ctx.runtime,
4620
+ abortSignal: ctx.abortSignal,
4621
+ statusSink: (patch) => {
4622
+ ctx.setStatus({
4623
+ ...ctx.getStatus(),
4624
+ ...patch
4625
+ });
4626
+ }
4627
+ });
4628
+ try {
4629
+ await waitUntilAbort(ctx.abortSignal);
4630
+ } finally {
4631
+ monitor.stop();
4632
+ ctx.setStatus({
4633
+ ...ctx.getStatus(),
4634
+ running: false,
4635
+ connected: false,
4636
+ lastStopAt: Date.now()
4637
+ });
4638
+ }
4639
+ },
4640
+ stopAccount: async (ctx) => {
4641
+ const client = getActiveAibotClient(ctx.accountId);
4642
+ client?.stop();
4643
+ ctx.setStatus({
4644
+ ...ctx.getStatus(),
4645
+ running: false,
4646
+ connected: false,
4647
+ lastStopAt: Date.now()
4648
+ });
4649
+ }
4650
+ }
4651
+ };
4652
+
4653
+ // index.ts
4654
+ var plugin = {
4655
+ id: "clawpool",
4656
+ name: "Clawpool OpenClaw",
4657
+ description: "Clawpool channel plugin backed by Aibot Agent API",
4658
+ configSchema: emptyPluginConfigSchema(),
4659
+ register(api) {
4660
+ setAibotRuntime(api.runtime);
4661
+ api.registerChannel({ plugin: aibotPlugin });
4662
+ }
4663
+ };
4664
+ var index_default = plugin;
4665
+ export {
4666
+ index_default as default
4667
+ };