@newbase-clawchat/openclaw-clawchat 2026.5.12-7 → 2026.5.12-9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -14
- package/dist/src/channel.setup.js +3 -16
- package/dist/src/inbound.js +0 -23
- package/dist/src/login.runtime.js +19 -0
- package/dist/src/outbound.js +85 -4
- package/dist/src/reply-dispatcher.js +123 -19
- package/dist/src/runtime.js +102 -0
- package/dist/src/storage.js +381 -0
- package/dist/src/tools.js +194 -104
- package/package.json +1 -1
- package/src/channel.outbound.test.ts +173 -12
- package/src/channel.setup.ts +7 -16
- package/src/channel.test.ts +18 -35
- package/src/inbound.test.ts +4 -8
- package/src/inbound.ts +0 -27
- package/src/login.runtime.test.ts +37 -0
- package/src/login.runtime.ts +23 -0
- package/src/outbound.test.ts +29 -0
- package/src/outbound.ts +101 -4
- package/src/reply-dispatcher.test.ts +398 -1
- package/src/reply-dispatcher.ts +143 -18
- package/src/runtime.test.ts +442 -0
- package/src/runtime.ts +122 -0
- package/src/storage.test.ts +200 -0
- package/src/storage.ts +571 -0
- package/src/tools.test.ts +89 -1
- package/src/tools.ts +228 -123
package/README.md
CHANGED
|
@@ -64,17 +64,18 @@ openclaw channels login --channel openclaw-clawchat
|
|
|
64
64
|
|
|
65
65
|
OpenClaw 2026.5.5 can load an npm-installed third-party channel while still
|
|
66
66
|
omitting it from the `channels add` CLI catalog. If `channels add` fails with
|
|
67
|
-
`Unknown channel: openclaw-clawchat`, use `/clawchat-login A1B2C3` after
|
|
68
|
-
Gateway restart
|
|
67
|
+
`Unknown channel: openclaw-clawchat`, use `/clawchat-login A1B2C3` after the
|
|
68
|
+
Gateway has loaded the installed plugin through config reload/hot restart, or
|
|
69
|
+
after a manual restart if automatic reload is unavailable.
|
|
69
70
|
|
|
70
71
|
After a successful activation on a running Gateway with config reload, OpenClaw
|
|
71
72
|
should load the full runtime plugin and start the channel automatically. If the
|
|
72
|
-
Gateway only has the setup-only entry loaded, the credential write
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
Gateway only has the setup-only entry loaded, the credential write lets
|
|
74
|
+
OpenClaw's config watcher hot-reload or hot-restart into the full runtime instead
|
|
75
|
+
of doing a setup-only channel reload; after the full runtime is attached, later
|
|
76
|
+
channel config changes can hot reload the channel. Restart the Gateway manually
|
|
77
|
+
only when config reload/hot restart is disabled or stalled, or when the channel
|
|
78
|
+
probe does not become healthy:
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
81
|
openclaw gateway restart
|
|
@@ -89,10 +90,11 @@ openclaw gateway run
|
|
|
89
90
|
|
|
90
91
|
The `--token` value above is the ClawChat invite code for OpenClaw's generic
|
|
91
92
|
`channels add` CLI surface on hosts that expose this plugin in the channel
|
|
92
|
-
catalog; the setup
|
|
93
|
-
token fields, default
|
|
94
|
-
`plugins.
|
|
95
|
-
|
|
93
|
+
catalog; the setup adapter validates the invite code without persisting a
|
|
94
|
+
pre-credential channel skeleton. Persisted token fields, default
|
|
95
|
+
`groupMode: "all"`, `plugins.entries.openclaw-clawchat`, `plugins.allow`, and
|
|
96
|
+
`tools.alsoAllow` are written together only after the invite code exchange
|
|
97
|
+
succeeds. The plugin registers the ClawChat account/media/search/moment tools
|
|
96
98
|
with the OpenClaw agent harness at plugin load time, and activation/login
|
|
97
99
|
preserves existing plugin entry fields, creates `plugins.allow` with
|
|
98
100
|
`openclaw-clawchat` when it is missing, appends the same id when it already
|
|
@@ -103,8 +105,7 @@ Operators who prefer quieter groups can set `groupMode: "mention"`; later
|
|
|
103
105
|
credential refreshes preserve that explicit choice.
|
|
104
106
|
Before activation, account/media tools return a config error instead of
|
|
105
107
|
disappearing; after activation/login, the channel is enabled and the same tools
|
|
106
|
-
read the persisted token/userId after the runtime plugin reloads or
|
|
107
|
-
restarts.
|
|
108
|
+
read the persisted token/userId after the runtime plugin reloads or hot-restarts.
|
|
108
109
|
|
|
109
110
|
After activation/login, the channel section is enabled and has credentials:
|
|
110
111
|
|
|
@@ -26,8 +26,8 @@ const configAdapter = createTopLevelChannelConfigAdapter({
|
|
|
26
26
|
/**
|
|
27
27
|
* Invite-code setup adapter used by OpenClaw setup surfaces.
|
|
28
28
|
*
|
|
29
|
-
* `channels add --token` passes the invite code as setup input. The
|
|
30
|
-
*
|
|
29
|
+
* `channels add --token` passes the invite code as setup input. The setup
|
|
30
|
+
* write leaves channel config unchanged; `afterAccountConfigWritten` exchanges
|
|
31
31
|
* the invite code and persists token/userId through the host runtime mutator.
|
|
32
32
|
*/
|
|
33
33
|
const setupAdapter = {
|
|
@@ -43,20 +43,7 @@ const setupAdapter = {
|
|
|
43
43
|
}
|
|
44
44
|
return null;
|
|
45
45
|
},
|
|
46
|
-
applyAccountConfig: ({ cfg }) =>
|
|
47
|
-
const channels = (cfg.channels ?? {});
|
|
48
|
-
const current = (channels[CHANNEL_ID] ?? {});
|
|
49
|
-
const groupMode = current.groupMode === "mention" || current.groupMode === "all"
|
|
50
|
-
? current.groupMode
|
|
51
|
-
: "all";
|
|
52
|
-
return {
|
|
53
|
-
...cfg,
|
|
54
|
-
channels: {
|
|
55
|
-
...channels,
|
|
56
|
-
[CHANNEL_ID]: { ...current, enabled: true, groupMode },
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
},
|
|
46
|
+
applyAccountConfig: ({ cfg }) => cfg,
|
|
60
47
|
afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
|
|
61
48
|
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
|
|
62
49
|
const code = typeof input.code === "string" && input.code.trim()
|
package/dist/src/inbound.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { EVENT, } from "./protocol-types.js";
|
|
2
2
|
import { extractMediaFragments, fragmentsToText } from "./message-mapper.js";
|
|
3
3
|
import { hasRenderableText, isInboundMessagePayload } from "./protocol.js";
|
|
4
|
-
const DEDUP_MAX = 256;
|
|
5
|
-
const dedupSeen = [];
|
|
6
|
-
const dedupSet = new Set();
|
|
7
4
|
function normalizeSender(sender) {
|
|
8
5
|
if (!sender || typeof sender !== "object")
|
|
9
6
|
return null;
|
|
@@ -39,22 +36,6 @@ function extractMentionIds(fragments) {
|
|
|
39
36
|
.map((fragment) => fragment.kind === "mention" ? fragment.user_id : undefined)
|
|
40
37
|
.filter((userId) => typeof userId === "string" && userId.length > 0);
|
|
41
38
|
}
|
|
42
|
-
export function _resetDedupForTest() {
|
|
43
|
-
dedupSeen.length = 0;
|
|
44
|
-
dedupSet.clear();
|
|
45
|
-
}
|
|
46
|
-
function rememberAndCheck(messageId) {
|
|
47
|
-
if (dedupSet.has(messageId))
|
|
48
|
-
return true;
|
|
49
|
-
dedupSet.add(messageId);
|
|
50
|
-
dedupSeen.push(messageId);
|
|
51
|
-
if (dedupSeen.length > DEDUP_MAX) {
|
|
52
|
-
const evict = dedupSeen.shift();
|
|
53
|
-
if (evict)
|
|
54
|
-
dedupSet.delete(evict);
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
39
|
/**
|
|
59
40
|
* Exported for direct unit testing. Direct chats always count as addressed;
|
|
60
41
|
* group chats require a mention unless config opts into all group messages.
|
|
@@ -107,10 +88,6 @@ export async function dispatchOpenclawClawlingInbound(params) {
|
|
|
107
88
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip empty msg=${payload.message_id}`);
|
|
108
89
|
return;
|
|
109
90
|
}
|
|
110
|
-
if (rememberAndCheck(payload.message_id)) {
|
|
111
|
-
log?.info?.(`[${account.accountId}] openclaw-clawchat skip duplicate msg=${payload.message_id}`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
91
|
const rawBody = fragmentsToText(message.body.fragments, {
|
|
115
92
|
mentionFallbackIds: message.context.mentions,
|
|
116
93
|
});
|
|
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
3
3
|
import { ClawlingApiError } from "./api-types.js";
|
|
4
4
|
import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
5
|
+
import { getClawChatStore } from "./storage.js";
|
|
5
6
|
/**
|
|
6
7
|
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
7
8
|
* agent runtime — openclaw's bundled clawchat channel.
|
|
@@ -130,6 +131,24 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
130
131
|
const tokenPreview = redactToken(result.access_token);
|
|
131
132
|
runtime.log(`Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${result.refresh_token ? " refreshToken=***" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
|
|
132
133
|
await persistLoginConfig(params, result);
|
|
134
|
+
try {
|
|
135
|
+
const store = params.store ??
|
|
136
|
+
getClawChatStore({
|
|
137
|
+
...(params.dbPath ? { dbPath: params.dbPath } : {}),
|
|
138
|
+
log: { error: runtime.log },
|
|
139
|
+
});
|
|
140
|
+
store.upsertActivation({
|
|
141
|
+
platform: "openclaw",
|
|
142
|
+
accountId: account.accountId,
|
|
143
|
+
userId: result.agent.user_id,
|
|
144
|
+
accessToken: result.access_token,
|
|
145
|
+
refreshToken: result.refresh_token ?? null,
|
|
146
|
+
loginMethod: "login",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
runtime.log("openclaw-clawchat sqlite activation persistence failed; login continues.");
|
|
151
|
+
}
|
|
133
152
|
runtime.log(`Config file updated.`);
|
|
134
153
|
runtime.log(`openclaw-clawchat login succeeded (user_id=${result.agent.user_id}, nickname=${result.agent.nickname || "-"}).`);
|
|
135
154
|
}
|
package/dist/src/outbound.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CHANNEL_ID, resolveOpenclawClawlingAccount } from "./config.js";
|
|
|
5
5
|
import { textToFragments } from "./message-mapper.js";
|
|
6
6
|
import { uploadOutboundMedia } from "./media-runtime.js";
|
|
7
7
|
import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawClawlingClient, } from "./runtime.js";
|
|
8
|
+
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
8
9
|
import { createAlignedWsQueue } from "./ws-alignment.js";
|
|
9
10
|
import { formatWsLog } from "./ws-log.js";
|
|
10
11
|
const alignedOutboundQueues = new WeakMap();
|
|
@@ -183,11 +184,13 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
183
184
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
184
185
|
const fragments = [...textFragments, ...richFragments, ...mediaFragments];
|
|
185
186
|
const useReply = Boolean(params.replyCtx);
|
|
187
|
+
const messageId = params.messageId;
|
|
186
188
|
let ack;
|
|
187
189
|
let mode;
|
|
188
190
|
if (useReply && params.replyCtx) {
|
|
189
191
|
mode = "reply";
|
|
190
192
|
const payload = {
|
|
193
|
+
...(messageId ? { message_id: messageId } : {}),
|
|
191
194
|
message_mode: "normal",
|
|
192
195
|
message: {
|
|
193
196
|
body: { fragments },
|
|
@@ -216,6 +219,7 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
216
219
|
else {
|
|
217
220
|
mode = "send";
|
|
218
221
|
const payload = {
|
|
222
|
+
...(messageId ? { message_id: messageId } : {}),
|
|
219
223
|
message_mode: "normal",
|
|
220
224
|
message: {
|
|
221
225
|
body: { fragments },
|
|
@@ -231,6 +235,9 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
231
235
|
...(params.log ? { log: params.log } : {}),
|
|
232
236
|
});
|
|
233
237
|
}
|
|
238
|
+
if (messageId && ack.payload.message_id !== messageId) {
|
|
239
|
+
throw new Error(`ack message_id mismatch: expected ${messageId} got ${ack.payload.message_id}`);
|
|
240
|
+
}
|
|
234
241
|
params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`);
|
|
235
242
|
return {
|
|
236
243
|
messageId: ack.payload.message_id,
|
|
@@ -257,11 +264,48 @@ export async function sendOpenclawClawlingMedia(params) {
|
|
|
257
264
|
to: params.to,
|
|
258
265
|
text: params.text ?? "",
|
|
259
266
|
mediaFragments: params.mediaFragments,
|
|
267
|
+
...(params.messageId ? { messageId: params.messageId } : {}),
|
|
260
268
|
...(params.replyCtx ? { replyCtx: params.replyCtx } : {}),
|
|
261
269
|
...(params.mentions ? { mentions: params.mentions } : {}),
|
|
262
270
|
...(params.log ? { log: params.log } : {}),
|
|
263
271
|
});
|
|
264
272
|
}
|
|
273
|
+
function mintOutboundMessageId(account) {
|
|
274
|
+
return `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
275
|
+
}
|
|
276
|
+
function resolveChannelOutboundStore() {
|
|
277
|
+
try {
|
|
278
|
+
const runtime = getOpenclawClawlingRuntime();
|
|
279
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
280
|
+
return getClawChatStore({
|
|
281
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function claimChannelOutbound(params) {
|
|
289
|
+
const store = resolveChannelOutboundStore();
|
|
290
|
+
if (!store)
|
|
291
|
+
return null;
|
|
292
|
+
try {
|
|
293
|
+
return store.claimMessageOnce({
|
|
294
|
+
platform: "openclaw",
|
|
295
|
+
accountId: params.account.accountId,
|
|
296
|
+
kind: "message",
|
|
297
|
+
direction: "outbound",
|
|
298
|
+
eventType: "message.send",
|
|
299
|
+
chatId: params.target.chatId,
|
|
300
|
+
messageId: params.messageId,
|
|
301
|
+
text: params.text,
|
|
302
|
+
raw: params.raw,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
265
309
|
export const openclawClawlingOutbound = {
|
|
266
310
|
deliveryMode: "direct",
|
|
267
311
|
chunker: (text, limit) => chunkMarkdownText(text, limit),
|
|
@@ -273,15 +317,35 @@ export const openclawClawlingOutbound = {
|
|
|
273
317
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
274
318
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
275
319
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
320
|
+
const target = parseOpenclawRecipient(to);
|
|
321
|
+
const messageId = mintOutboundMessageId(account);
|
|
322
|
+
const trimmedText = text.trim();
|
|
323
|
+
if (!trimmedText) {
|
|
324
|
+
throw new Error("openclaw-clawchat sendText requires non-empty text");
|
|
325
|
+
}
|
|
326
|
+
const claimed = claimChannelOutbound({
|
|
327
|
+
account,
|
|
328
|
+
target,
|
|
329
|
+
messageId,
|
|
330
|
+
text: trimmedText,
|
|
331
|
+
raw: { target, mode: "channel-sendText" },
|
|
332
|
+
});
|
|
333
|
+
if (claimed === false) {
|
|
334
|
+
throw new Error("openclaw-clawchat outbound duplicate claim; message not sent");
|
|
335
|
+
}
|
|
336
|
+
if (claimed === null) {
|
|
337
|
+
throw new Error("openclaw-clawchat outbound message claim failed");
|
|
338
|
+
}
|
|
276
339
|
const result = await sendOpenclawClawlingText({
|
|
277
340
|
client,
|
|
278
341
|
account,
|
|
279
|
-
to:
|
|
342
|
+
to: target,
|
|
280
343
|
text,
|
|
344
|
+
messageId,
|
|
281
345
|
});
|
|
282
346
|
return {
|
|
283
347
|
to,
|
|
284
|
-
messageId: result?.messageId ??
|
|
348
|
+
messageId: result?.messageId ?? messageId,
|
|
285
349
|
};
|
|
286
350
|
},
|
|
287
351
|
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
@@ -307,16 +371,33 @@ export const openclawClawlingOutbound = {
|
|
|
307
371
|
if (mediaFragments.length === 0) {
|
|
308
372
|
throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);
|
|
309
373
|
}
|
|
374
|
+
const target = parseOpenclawRecipient(to);
|
|
375
|
+
const messageId = mintOutboundMessageId(account);
|
|
376
|
+
const claimText = (text ?? "").trim();
|
|
377
|
+
const claimed = claimChannelOutbound({
|
|
378
|
+
account,
|
|
379
|
+
target,
|
|
380
|
+
messageId,
|
|
381
|
+
text: claimText,
|
|
382
|
+
raw: { target, mode: "channel-sendMedia", mediaCount: mediaFragments.length },
|
|
383
|
+
});
|
|
384
|
+
if (claimed === false) {
|
|
385
|
+
throw new Error("openclaw-clawchat outbound duplicate claim; message not sent");
|
|
386
|
+
}
|
|
387
|
+
if (claimed === null) {
|
|
388
|
+
throw new Error("openclaw-clawchat outbound message claim failed");
|
|
389
|
+
}
|
|
310
390
|
const result = await sendOpenclawClawlingMedia({
|
|
311
391
|
client,
|
|
312
392
|
account,
|
|
313
|
-
to:
|
|
393
|
+
to: target,
|
|
314
394
|
text,
|
|
315
395
|
mediaFragments,
|
|
396
|
+
messageId,
|
|
316
397
|
});
|
|
317
398
|
return {
|
|
318
399
|
to,
|
|
319
|
-
messageId: result?.messageId ??
|
|
400
|
+
messageId: result?.messageId ?? messageId,
|
|
320
401
|
};
|
|
321
402
|
},
|
|
322
403
|
}),
|
|
@@ -5,7 +5,7 @@ import { openBufferedStreamingSession, mergeStreamingText, } from "./buffered-st
|
|
|
5
5
|
import { emitFinalStreamReply } from "./client.js";
|
|
6
6
|
import { textToFragments } from "./message-mapper.js";
|
|
7
7
|
import { uploadOutboundMedia } from "./media-runtime.js";
|
|
8
|
-
import { sendOpenclawClawlingText } from "./outbound.js";
|
|
8
|
+
import { sendOpenclawClawlingText, } from "./outbound.js";
|
|
9
9
|
import { sendStreamingFailure } from "./streaming.js";
|
|
10
10
|
const CLIENT_SAFE_REPLY_FAILURE_TEXT = "OpenClaw could not complete this reply.";
|
|
11
11
|
function normalizeReplyErrorText(error) {
|
|
@@ -119,7 +119,7 @@ function resolvePayloadText(payload) {
|
|
|
119
119
|
* `message.reply` per deliver with text + media.
|
|
120
120
|
*/
|
|
121
121
|
export function createOpenclawClawlingReplyDispatcher(options) {
|
|
122
|
-
const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, inboundForFinalReply, log, } = options;
|
|
122
|
+
const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, inboundForFinalReply, store, log, } = options;
|
|
123
123
|
const routing = { chatId: target.chatId, chatType: target.chatType };
|
|
124
124
|
const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
|
|
125
125
|
const streamingEnabled = account.replyMode === "stream" && !replyCtx;
|
|
@@ -153,16 +153,87 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
153
153
|
let streamingClosed = false;
|
|
154
154
|
let runFailed = false;
|
|
155
155
|
let runDone = false;
|
|
156
|
+
let streamClaimAttempted = false;
|
|
156
157
|
// `streamCreatedEmitted` is the authoritative guard: once a `message.created`
|
|
157
158
|
// has been emitted for this dispatcher instance, never emit another — even
|
|
158
159
|
// if `onReplyStart` fires again or a pre-onReplyStart `onPartialReply`
|
|
159
160
|
// raced the lazy open path.
|
|
160
161
|
let streamCreatedEmitted = false;
|
|
162
|
+
const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
|
|
163
|
+
const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
|
|
164
|
+
const claimOutbound = (eventType, messageId, text, raw) => {
|
|
165
|
+
if (!store || !messageId)
|
|
166
|
+
return null;
|
|
167
|
+
try {
|
|
168
|
+
return store.claimMessageOnce({
|
|
169
|
+
platform: "openclaw",
|
|
170
|
+
accountId: account.accountId,
|
|
171
|
+
kind: "message",
|
|
172
|
+
direction: "outbound",
|
|
173
|
+
eventType,
|
|
174
|
+
chatId: target.chatId,
|
|
175
|
+
messageId,
|
|
176
|
+
text,
|
|
177
|
+
raw,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound claim failed`);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const updateOutbound = (eventType, messageId, text, raw) => {
|
|
186
|
+
if (!store || !messageId)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
store.updateMessageByIdentity({
|
|
190
|
+
accountId: account.accountId,
|
|
191
|
+
kind: "message",
|
|
192
|
+
direction: "outbound",
|
|
193
|
+
eventType,
|
|
194
|
+
chatId: target.chatId,
|
|
195
|
+
messageId,
|
|
196
|
+
text,
|
|
197
|
+
raw,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound update failed; continuing`);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const recordOutbound = (kind, messageId, text) => {
|
|
205
|
+
if (!store || !messageId)
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
store.insertMessage({
|
|
209
|
+
platform: "openclaw",
|
|
210
|
+
accountId: account.accountId,
|
|
211
|
+
kind,
|
|
212
|
+
direction: "outbound",
|
|
213
|
+
eventType: outboundEventType(),
|
|
214
|
+
chatId: target.chatId,
|
|
215
|
+
messageId,
|
|
216
|
+
text,
|
|
217
|
+
raw: outboundRaw(),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound insert failed; continuing`);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const recordThinkingIfLinked = (messageId) => {
|
|
225
|
+
const thinkingText = reasoningText.trim();
|
|
226
|
+
if (!thinkingText)
|
|
227
|
+
return;
|
|
228
|
+
recordOutbound("thinking", messageId, thinkingText);
|
|
229
|
+
reasoningText = "";
|
|
230
|
+
};
|
|
161
231
|
const mintStreamingMessageId = () => `${account.userId}-stream-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
232
|
+
const mintStaticMessageId = () => `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
162
233
|
const openSessionIfNeeded = () => {
|
|
163
|
-
if (!streamingEnabled || streamingSession || streamCreatedEmitted)
|
|
234
|
+
if (!streamingEnabled || streamingSession || streamCreatedEmitted || streamClaimAttempted)
|
|
164
235
|
return;
|
|
165
|
-
|
|
236
|
+
streamClaimAttempted = true;
|
|
166
237
|
// Mint a fresh agent-side message_id at `message.created` time. All
|
|
167
238
|
// subsequent `message.add` / `message.done` / `message.reply` frames for
|
|
168
239
|
// this stream reuse it. Once the stream finalizes (done or reply), this
|
|
@@ -171,6 +242,14 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
171
242
|
// `replyTo.msgId`; keeping the two distinct avoids the agent's reply
|
|
172
243
|
// frames shadowing the user turn they answer.
|
|
173
244
|
streamingMessageId = mintStreamingMessageId();
|
|
245
|
+
const claimed = claimOutbound("message.created", streamingMessageId, "", { target, replyCtx: replyCtx ?? null, mode: "stream" });
|
|
246
|
+
if (claimed !== true) {
|
|
247
|
+
streamCreatedEmitted = false;
|
|
248
|
+
streamingMessageId = "";
|
|
249
|
+
log?.[claimed === false ? "info" : "error"]?.(`[${account.accountId}] openclaw-clawchat stream outbound skipped reason=${claimed === false ? "duplicate" : "claim_unavailable"}`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
streamCreatedEmitted = true;
|
|
174
253
|
streamingSession = openBufferedStreamingSession({
|
|
175
254
|
client,
|
|
176
255
|
routing,
|
|
@@ -217,21 +296,36 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
217
296
|
log?.info?.(`[${account.accountId}] openclaw-clawchat streaming closed msg=${streamingMessageId} reason=${reason ?? "done"}`);
|
|
218
297
|
};
|
|
219
298
|
// ----- Static send ------------------------------------------------------
|
|
220
|
-
const sendStatic = async (text, mediaFragments = [], richFragments = []) => {
|
|
299
|
+
const sendStatic = async (text, mediaFragments = [], richFragments = [], options = {}) => {
|
|
221
300
|
if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0)
|
|
222
|
-
return;
|
|
301
|
+
return null;
|
|
223
302
|
log?.info?.(`[${account.accountId}] openclaw-clawchat sending static text_len=${text.length} media=${mediaFragments.length} rich=${richFragments.length} to=${target.chatId}`);
|
|
224
|
-
|
|
303
|
+
const messageId = mintStaticMessageId();
|
|
304
|
+
const raw = { target, replyCtx: replyCtx ?? null, mode: "static" };
|
|
305
|
+
const claimed = options.recordMessage
|
|
306
|
+
? claimOutbound(outboundEventType(), messageId, text, raw)
|
|
307
|
+
: true;
|
|
308
|
+
if (claimed === false) {
|
|
309
|
+
log?.info?.(`[${account.accountId}] openclaw-clawchat outbound duplicate skipped msg=${messageId}`);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (claimed === null) {
|
|
313
|
+
log?.error?.(`[${account.accountId}] openclaw-clawchat outbound skipped msg=${messageId} reason=claim_unavailable`);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const result = await sendOpenclawClawlingText({
|
|
225
317
|
client,
|
|
226
318
|
account,
|
|
227
319
|
to: target,
|
|
228
320
|
text,
|
|
321
|
+
messageId,
|
|
229
322
|
...(replyCtx ? { replyCtx } : {}),
|
|
230
323
|
...(richFragments.length > 0 ? { richFragments } : {}),
|
|
231
324
|
...(mediaFragments.length > 0 ? { mediaFragments } : {}),
|
|
232
325
|
log,
|
|
233
326
|
});
|
|
234
327
|
log?.info?.(`[${account.accountId}] openclaw-clawchat send complete to=${target.chatId}`);
|
|
328
|
+
return result;
|
|
235
329
|
};
|
|
236
330
|
const logDetachedFailure = (action, error) => {
|
|
237
331
|
log?.error?.(`[${account.accountId}] openclaw-clawchat ${action} failed: ${String(error)}`);
|
|
@@ -243,6 +337,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
243
337
|
if (finalEmitted)
|
|
244
338
|
return;
|
|
245
339
|
finalEmitted = true;
|
|
340
|
+
if (!streamingMessageId)
|
|
341
|
+
return;
|
|
246
342
|
const mergedMedia = await uploadMediaUrls(accumulatedMediaUrls.slice());
|
|
247
343
|
const mergedText = streamText.trim();
|
|
248
344
|
if (!mergedText && finalRichFragments.length === 0 && mergedMedia.length === 0) {
|
|
@@ -272,6 +368,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
272
368
|
},
|
|
273
369
|
body: { fragments: bodyFragments },
|
|
274
370
|
});
|
|
371
|
+
updateOutbound("message.reply", streamingMessageId, mergedText, { target, replyCtx: replyCtx ?? null, mode: "stream-final" });
|
|
372
|
+
recordThinkingIfLinked(streamingMessageId);
|
|
275
373
|
};
|
|
276
374
|
const ingestFinalPayload = (payload, text, richFragment) => {
|
|
277
375
|
if (richFragment && account.richInteractions) {
|
|
@@ -328,7 +426,10 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
328
426
|
await queueStreamSnapshot();
|
|
329
427
|
}
|
|
330
428
|
else {
|
|
331
|
-
|
|
429
|
+
reasoningText = mergeStreamingText(reasoningText, text);
|
|
430
|
+
const result = await sendStatic(text, [], [], { recordMessage: true });
|
|
431
|
+
if (result?.messageId)
|
|
432
|
+
recordThinkingIfLinked(result.messageId);
|
|
332
433
|
}
|
|
333
434
|
return;
|
|
334
435
|
}
|
|
@@ -338,9 +439,19 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
338
439
|
ingestFinalPayload(payload, text, richFragment && account.richInteractions ? richFragment : null);
|
|
339
440
|
// For streaming: consolidated final is emitted in onIdle after done.
|
|
340
441
|
// For static: emit immediately.
|
|
341
|
-
if (
|
|
442
|
+
if (streamingEnabled) {
|
|
443
|
+
if (text.trim()) {
|
|
444
|
+
await queueStreamSnapshot();
|
|
445
|
+
}
|
|
446
|
+
else if (richFragment || urls.length > 0) {
|
|
447
|
+
openSessionIfNeeded();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
342
451
|
const mediaFragments = await uploadMediaUrls(urls);
|
|
343
|
-
await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : []);
|
|
452
|
+
const result = await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
|
|
453
|
+
if (result?.messageId)
|
|
454
|
+
recordThinkingIfLinked(result.messageId);
|
|
344
455
|
}
|
|
345
456
|
return;
|
|
346
457
|
}
|
|
@@ -352,14 +463,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
352
463
|
const mediaFragments = await uploadMediaUrls(urls);
|
|
353
464
|
if (mediaFragments.length > 0) {
|
|
354
465
|
log?.info?.(`[${account.accountId}] openclaw-clawchat mid-stream media emitted as separate message (count=${mediaFragments.length})`);
|
|
355
|
-
await
|
|
356
|
-
client,
|
|
357
|
-
account,
|
|
358
|
-
to: target,
|
|
359
|
-
text: "",
|
|
360
|
-
mediaFragments,
|
|
361
|
-
log,
|
|
362
|
-
});
|
|
466
|
+
await sendStatic("", mediaFragments, [], { recordMessage: true });
|
|
363
467
|
}
|
|
364
468
|
}
|
|
365
469
|
}
|
|
@@ -367,7 +471,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
367
471
|
const mediaFragments = await uploadMediaUrls(urls);
|
|
368
472
|
const richFragments = richFragment && account.richInteractions ? [richFragment] : [];
|
|
369
473
|
if (text.trim() || mediaFragments.length > 0 || richFragments.length > 0) {
|
|
370
|
-
await sendStatic(text, mediaFragments, richFragments);
|
|
474
|
+
await sendStatic(text, mediaFragments, richFragments, { recordMessage: true });
|
|
371
475
|
}
|
|
372
476
|
}
|
|
373
477
|
},
|