@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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 +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/dist/src/runtime.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "
|
|
1
|
+
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, EVENT, } from "./protocol-types.js";
|
|
2
2
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
3
|
+
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
3
4
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
4
5
|
import { createOpenclawClawlingClient } from "./client.js";
|
|
5
|
-
import {
|
|
6
|
+
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
7
|
+
import { ClawlingApiError } from "./api-types.js";
|
|
8
|
+
import { CHANNEL_ID, effectiveGroupCommandMode, } from "./config.js";
|
|
6
9
|
import { dispatchOpenclawClawlingInbound } from "./inbound.js";
|
|
7
10
|
import { fetchInboundMedia } from "./media-runtime.js";
|
|
8
11
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.js";
|
|
@@ -10,12 +13,69 @@ import { sendStreamingText } from "./streaming.js";
|
|
|
10
13
|
import { flushAlignedOutboundQueue, getAlignedOutboundQueueSize, setAlignedOutboundLogContext, } from "./outbound.js";
|
|
11
14
|
import { formatWsLog } from "./ws-log.js";
|
|
12
15
|
import { createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.js";
|
|
16
|
+
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
17
|
+
import { getClawChatGroupPrompt, getClawChatUserPrompt } from "./plugin-prompts.js";
|
|
18
|
+
import { loadClawChatPromptMetadata, renderClawChatProfilePrompt, resolveSenderRelation, } from "./profile-prompt.js";
|
|
19
|
+
import { refreshGroupProfile, syncFirstSeenClawChatProfiles } from "./profile-sync.js";
|
|
20
|
+
import { pullGroupMetadata, pullOwnerMetadata } from "./clawchat-metadata.js";
|
|
21
|
+
import { clearClawChatPromptInjectionForSession, stageClawChatPromptInjection, } from "./prompt-injection.js";
|
|
22
|
+
import { createGroupMessageCoalescer } from "./group-message-coalescer.js";
|
|
13
23
|
const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } = createPluginRuntimeStore("openclaw-clawchat runtime not initialized");
|
|
14
24
|
export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
|
|
15
25
|
const activeClients = new Map();
|
|
26
|
+
const CLAWCHAT_PLUGIN_SLASH_COMMANDS = new Set(["clawchat-activate"]);
|
|
27
|
+
const CLAWCHAT_MEMORY_ROOT_UNAVAILABLE = "ClawChat memory root unavailable: OpenClaw workspaceDir could not be resolved";
|
|
28
|
+
const OPENCLAW_CONFIRM_SLASH_COMMANDS = new Set([
|
|
29
|
+
"approve",
|
|
30
|
+
"deny",
|
|
31
|
+
"always",
|
|
32
|
+
"cancel",
|
|
33
|
+
"yes",
|
|
34
|
+
"no",
|
|
35
|
+
"ok",
|
|
36
|
+
"confirm",
|
|
37
|
+
"remember",
|
|
38
|
+
"nevermind",
|
|
39
|
+
]);
|
|
40
|
+
function parseSlashCommandName(rawBody) {
|
|
41
|
+
const stripped = rawBody.trimStart();
|
|
42
|
+
if (!stripped.startsWith("/"))
|
|
43
|
+
return null;
|
|
44
|
+
const token = stripped.split(/\s+/, 1)[0] ?? "";
|
|
45
|
+
const name = token.slice(1).replace(/_/g, "-").toLowerCase();
|
|
46
|
+
if (!name || name.includes("/"))
|
|
47
|
+
return null;
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
function isKnownOpenClawGroupSlashCommand(rawBody, cfg) {
|
|
51
|
+
const name = parseSlashCommandName(rawBody);
|
|
52
|
+
if (!name)
|
|
53
|
+
return false;
|
|
54
|
+
return hasControlCommand(rawBody, cfg)
|
|
55
|
+
|| CLAWCHAT_PLUGIN_SLASH_COMMANDS.has(name)
|
|
56
|
+
|| OPENCLAW_CONFIRM_SLASH_COMMANDS.has(name);
|
|
57
|
+
}
|
|
16
58
|
export function getOpenclawClawlingClient(accountId) {
|
|
17
59
|
return activeClients.get(accountId);
|
|
18
60
|
}
|
|
61
|
+
export function resolveClawChatMemoryRoot(runtime, cfg, agentId) {
|
|
62
|
+
const resolver = runtime.agent?.resolveAgentWorkspaceDir;
|
|
63
|
+
if (typeof resolver !== "function") {
|
|
64
|
+
throw new Error(CLAWCHAT_MEMORY_ROOT_UNAVAILABLE);
|
|
65
|
+
}
|
|
66
|
+
let workspaceDir;
|
|
67
|
+
try {
|
|
68
|
+
workspaceDir = resolver(cfg, agentId);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(CLAWCHAT_MEMORY_ROOT_UNAVAILABLE);
|
|
72
|
+
}
|
|
73
|
+
const memoryRoot = typeof workspaceDir === "string" ? workspaceDir.trim() : "";
|
|
74
|
+
if (!memoryRoot) {
|
|
75
|
+
throw new Error(CLAWCHAT_MEMORY_ROOT_UNAVAILABLE);
|
|
76
|
+
}
|
|
77
|
+
return memoryRoot;
|
|
78
|
+
}
|
|
19
79
|
export async function waitForOpenclawClawlingClient(accountId, options = {}) {
|
|
20
80
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
21
81
|
const pollMs = options.pollMs ?? 100;
|
|
@@ -64,6 +124,74 @@ export function classifyClawlingClientError(err) {
|
|
|
64
124
|
function formatConversationSubject(peer) {
|
|
65
125
|
return peer.kind === "group" ? `group:${peer.id}` : peer.id;
|
|
66
126
|
}
|
|
127
|
+
function parseApiTimestamp(value) {
|
|
128
|
+
if (typeof value !== "string")
|
|
129
|
+
return null;
|
|
130
|
+
const parsed = Date.parse(value);
|
|
131
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
132
|
+
}
|
|
133
|
+
function asRecord(value) {
|
|
134
|
+
return value && typeof value === "object" ? value : null;
|
|
135
|
+
}
|
|
136
|
+
function optionalString(value) {
|
|
137
|
+
return typeof value === "string" ? value : undefined;
|
|
138
|
+
}
|
|
139
|
+
function hasOwn(record, key) {
|
|
140
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
141
|
+
}
|
|
142
|
+
function optionalNullableString(record, key) {
|
|
143
|
+
if (!hasOwn(record, key))
|
|
144
|
+
return undefined;
|
|
145
|
+
const value = record[key];
|
|
146
|
+
if (value === null)
|
|
147
|
+
return null;
|
|
148
|
+
return optionalString(value);
|
|
149
|
+
}
|
|
150
|
+
function isConversationNotFoundError(err) {
|
|
151
|
+
if (!(err instanceof ClawlingApiError))
|
|
152
|
+
return false;
|
|
153
|
+
return err.meta?.status === 404 || err.meta?.status === 410 ||
|
|
154
|
+
err.meta?.code === 404 || err.meta?.code === 410 || err.meta?.code === 40401;
|
|
155
|
+
}
|
|
156
|
+
function metadataVersionFromEnvelope(env) {
|
|
157
|
+
const payload = asRecord(env.payload);
|
|
158
|
+
const version = payload?.version;
|
|
159
|
+
return typeof version === "number" && Number.isFinite(version) ? version : undefined;
|
|
160
|
+
}
|
|
161
|
+
function metadataScopesFromEnvelope(env) {
|
|
162
|
+
const scope = asRecord(env.payload)?.scope;
|
|
163
|
+
return Array.isArray(scope) ? scope.filter((item) => typeof item === "string") : [];
|
|
164
|
+
}
|
|
165
|
+
function shouldRefreshBehaviorForScopes(scopes) {
|
|
166
|
+
return scopes.includes("behavior");
|
|
167
|
+
}
|
|
168
|
+
function shouldRefreshConversationForScopes(scopes) {
|
|
169
|
+
if (scopes.length === 0)
|
|
170
|
+
return true;
|
|
171
|
+
return scopes.some((scope) => scope === "title" || scope === "description" || scope !== "behavior");
|
|
172
|
+
}
|
|
173
|
+
function buildConversationDetailsCacheInput(params) {
|
|
174
|
+
const { accountId, conversation, metadataVersion } = params;
|
|
175
|
+
const refreshedAt = Date.now();
|
|
176
|
+
const participants = Array.isArray(conversation.participants) ? conversation.participants : [];
|
|
177
|
+
return {
|
|
178
|
+
platform: "openclaw",
|
|
179
|
+
accountId,
|
|
180
|
+
conversationId: conversation.id,
|
|
181
|
+
conversationType: conversation.type,
|
|
182
|
+
...(metadataVersion !== undefined ? { metadataVersion } : {}),
|
|
183
|
+
lastSeenAt: parseApiTimestamp(conversation.updated_at),
|
|
184
|
+
lastRefreshedAt: refreshedAt,
|
|
185
|
+
raw: conversation,
|
|
186
|
+
members: participants.map((participant) => ({
|
|
187
|
+
userId: participant.user_id,
|
|
188
|
+
role: participant.role,
|
|
189
|
+
raw: participant,
|
|
190
|
+
lastSeenAt: parseApiTimestamp(participant.joined_at),
|
|
191
|
+
})),
|
|
192
|
+
membersComplete: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
67
195
|
function withClawChatSessionScope(cfg) {
|
|
68
196
|
return {
|
|
69
197
|
...cfg,
|
|
@@ -73,19 +201,92 @@ function withClawChatSessionScope(cfg) {
|
|
|
73
201
|
},
|
|
74
202
|
};
|
|
75
203
|
}
|
|
204
|
+
function buildActivationBootstrapText() {
|
|
205
|
+
return [
|
|
206
|
+
"ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
|
|
207
|
+
"Please do both:",
|
|
208
|
+
"1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
|
|
209
|
+
"2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
|
|
210
|
+
"Do not ask the user for profile information just for this bootstrap.",
|
|
211
|
+
].join("\n");
|
|
212
|
+
}
|
|
213
|
+
function buildActivationBootstrapEnvelope(params) {
|
|
214
|
+
const text = buildActivationBootstrapText();
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
return {
|
|
217
|
+
version: "2",
|
|
218
|
+
event: EVENT.MESSAGE_SEND,
|
|
219
|
+
trace_id: `openclaw-clawchat-bootstrap-${now}`,
|
|
220
|
+
emitted_at: now,
|
|
221
|
+
chat_id: params.conversationId,
|
|
222
|
+
chat_type: "direct",
|
|
223
|
+
to: { id: params.account.userId, type: "direct" },
|
|
224
|
+
sender: {
|
|
225
|
+
id: "clawchat-bootstrap",
|
|
226
|
+
type: "direct",
|
|
227
|
+
nick_name: "ClawChat Activation",
|
|
228
|
+
},
|
|
229
|
+
payload: {
|
|
230
|
+
message_id: `openclaw-clawchat-bootstrap-${params.conversationId}-${now}`,
|
|
231
|
+
message_mode: "normal",
|
|
232
|
+
message: {
|
|
233
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
234
|
+
context: { mentions: [], reply: null },
|
|
235
|
+
streaming: {
|
|
236
|
+
status: "static",
|
|
237
|
+
sequence: 0,
|
|
238
|
+
mutation_policy: "sealed",
|
|
239
|
+
started_at: null,
|
|
240
|
+
completed_at: null,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function resolveConnectionStore(params, runtime) {
|
|
247
|
+
if (params.store !== undefined)
|
|
248
|
+
return params.store;
|
|
249
|
+
if (params.transport)
|
|
250
|
+
return null;
|
|
251
|
+
try {
|
|
252
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
253
|
+
return getClawChatStore({
|
|
254
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
255
|
+
log: { error: (message) => params.log?.error?.(message) },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
params.log?.error?.("openclaw-clawchat sqlite connection persistence unavailable; continuing.");
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
76
263
|
export async function startOpenclawClawlingGateway(params) {
|
|
77
264
|
const { cfg, account, abortSignal, setStatus, getStatus, log } = params;
|
|
78
265
|
// Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
|
|
79
266
|
const runtime = getOpenclawClawlingRuntime();
|
|
80
267
|
const accountId = account.accountId;
|
|
81
|
-
|
|
268
|
+
const store = resolveConnectionStore(params, runtime);
|
|
269
|
+
let conversationApiClient;
|
|
270
|
+
const getConversationApiClient = () => {
|
|
271
|
+
conversationApiClient ??= createOpenclawClawlingApiClient({
|
|
272
|
+
baseUrl: account.baseUrl,
|
|
273
|
+
token: account.token,
|
|
274
|
+
userId: account.userId,
|
|
275
|
+
});
|
|
276
|
+
return conversationApiClient;
|
|
277
|
+
};
|
|
278
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} hasOwnerUserId=${Boolean(account.ownerUserId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
|
|
82
279
|
let lastHelloFailTraceId = "-";
|
|
83
280
|
let lastHelloFailReason = "";
|
|
84
281
|
let lastConnectTraceId = "-";
|
|
282
|
+
let lastHelloOkDeviceId;
|
|
283
|
+
let lastHelloOkDeliveryMode;
|
|
85
284
|
let currentAttemptStartedAt = 0;
|
|
86
285
|
let authFailureLogged = false;
|
|
87
286
|
let closingForAbort = false;
|
|
88
287
|
let wsReady = false;
|
|
288
|
+
let currentConnectionId = null;
|
|
289
|
+
let currentConnectionFinished = false;
|
|
89
290
|
const reconnectTracker = createReconnectTracker({
|
|
90
291
|
accountId,
|
|
91
292
|
log: (msg) => log?.info?.(msg),
|
|
@@ -99,11 +300,220 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
99
300
|
state: snapshot.state === "connected" ? "ready" : snapshot.state,
|
|
100
301
|
};
|
|
101
302
|
};
|
|
303
|
+
const recordConnection = (action, fn) => {
|
|
304
|
+
try {
|
|
305
|
+
return fn();
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
log?.error?.(`[${accountId}] openclaw-clawchat sqlite ${action} failed; continuing`);
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const finishCurrentConnection = (input) => {
|
|
313
|
+
if (!store || currentConnectionId == null || currentConnectionFinished)
|
|
314
|
+
return;
|
|
315
|
+
const connectionId = currentConnectionId;
|
|
316
|
+
recordConnection("finish", () => store.finishConnection(connectionId, input));
|
|
317
|
+
currentConnectionFinished = true;
|
|
318
|
+
currentConnectionId = null;
|
|
319
|
+
};
|
|
320
|
+
const refreshConversationDetails = async (conversationId, options) => {
|
|
321
|
+
try {
|
|
322
|
+
const memoryRoot = options.memoryRoot;
|
|
323
|
+
const data = memoryRoot
|
|
324
|
+
? await (async () => {
|
|
325
|
+
const result = await pullGroupMetadata({
|
|
326
|
+
memoryRoot,
|
|
327
|
+
groupId: conversationId,
|
|
328
|
+
api: getConversationApiClient(),
|
|
329
|
+
});
|
|
330
|
+
if (result.failures.length > 0) {
|
|
331
|
+
log?.error?.(`[${accountId}] openclaw-clawchat group participant metadata refresh partially failed: ${result.failures.map((failure) => `${failure.targetId}: ${failure.error}`).join("; ")}`);
|
|
332
|
+
}
|
|
333
|
+
if (!result.conversation)
|
|
334
|
+
throw new Error("ClawChat conversation metadata response is missing conversation");
|
|
335
|
+
return { conversation: result.conversation };
|
|
336
|
+
})()
|
|
337
|
+
: await getConversationApiClient().getConversation(conversationId);
|
|
338
|
+
if (!store?.upsertConversationDetails)
|
|
339
|
+
return;
|
|
340
|
+
recordConnection("conversation details upsert", () => store.upsertConversationDetails?.(buildConversationDetailsCacheInput({
|
|
341
|
+
accountId,
|
|
342
|
+
conversation: data.conversation,
|
|
343
|
+
...(options.metadataVersion !== undefined ? { metadataVersion: options.metadataVersion } : {}),
|
|
344
|
+
})));
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
if (isConversationNotFoundError(err)) {
|
|
348
|
+
if (store?.deleteConversationCache) {
|
|
349
|
+
recordConnection("conversation cache delete", () => store.deleteConversationCache?.({
|
|
350
|
+
platform: "openclaw",
|
|
351
|
+
accountId,
|
|
352
|
+
conversationId,
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
log?.error?.(`[${accountId}] openclaw-clawchat metadata refresh failed source=${options.source} conversation=${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const refreshAgentBehavior = async (options) => {
|
|
361
|
+
try {
|
|
362
|
+
await pullOwnerMetadata({
|
|
363
|
+
memoryRoot: options.memoryRoot,
|
|
364
|
+
agentId: account.agentId,
|
|
365
|
+
accountUserId: account.userId,
|
|
366
|
+
accountOwnerUserId: account.ownerUserId,
|
|
367
|
+
api: getConversationApiClient(),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
log?.error?.(`[${accountId}] openclaw-clawchat behavior refresh failed source=${options.source} agent=${account.userId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
const refreshConversationCacheAfterReady = async () => {
|
|
375
|
+
if (!store)
|
|
376
|
+
return;
|
|
377
|
+
const ids = [];
|
|
378
|
+
const seen = new Set();
|
|
379
|
+
const addId = (id) => {
|
|
380
|
+
if (typeof id !== "string" || !id || seen.has(id))
|
|
381
|
+
return;
|
|
382
|
+
seen.add(id);
|
|
383
|
+
ids.push(id);
|
|
384
|
+
};
|
|
385
|
+
const activation = store.getActivationConversation
|
|
386
|
+
? recordConnection("activation conversation read", () => store.getActivationConversation?.({ platform: "openclaw", accountId }))
|
|
387
|
+
: null;
|
|
388
|
+
addId(activation?.conversationId);
|
|
389
|
+
const cachedIds = store.listCachedConversationIds
|
|
390
|
+
? recordConnection("cached conversation ids read", () => store.listCachedConversationIds?.({ platform: "openclaw", accountId, limit: 20 })) ?? []
|
|
391
|
+
: [];
|
|
392
|
+
for (const id of cachedIds.slice(0, 20))
|
|
393
|
+
addId(id);
|
|
394
|
+
for (const id of ids) {
|
|
395
|
+
await refreshConversationDetails(id, { source: "reconnect" });
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const resolveMemoryRootForPeer = (peer) => {
|
|
399
|
+
try {
|
|
400
|
+
const route = runtime.channel.routing.resolveAgentRoute({
|
|
401
|
+
cfg: withClawChatSessionScope(cfg),
|
|
402
|
+
channel: CHANNEL_ID,
|
|
403
|
+
accountId,
|
|
404
|
+
peer,
|
|
405
|
+
});
|
|
406
|
+
return resolveClawChatMemoryRoot(runtime, cfg, route.agentId);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
log?.error?.(`[${accountId}] openclaw-clawchat metadata refresh memory root unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const handleMetadataInvalidation = async (env) => {
|
|
414
|
+
const conversationId = typeof env.chat_id === "string" && env.chat_id.trim()
|
|
415
|
+
? env.chat_id
|
|
416
|
+
: "";
|
|
417
|
+
if (!conversationId) {
|
|
418
|
+
log?.info?.(`[${accountId}] openclaw-clawchat metadata invalidation missing chat_id trace=${env.trace_id}`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const version = metadataVersionFromEnvelope(env);
|
|
422
|
+
const scopes = metadataScopesFromEnvelope(env);
|
|
423
|
+
const refreshBehavior = shouldRefreshBehaviorForScopes(scopes);
|
|
424
|
+
const refreshConversation = shouldRefreshConversationForScopes(scopes);
|
|
425
|
+
if (!refreshBehavior && !refreshConversation)
|
|
426
|
+
return;
|
|
427
|
+
const peer = {
|
|
428
|
+
kind: env.chat_type === "group" ? "group" : "direct",
|
|
429
|
+
id: conversationId,
|
|
430
|
+
};
|
|
431
|
+
const memoryRoot = resolveMemoryRootForPeer(peer);
|
|
432
|
+
if (!memoryRoot)
|
|
433
|
+
return;
|
|
434
|
+
if (refreshBehavior) {
|
|
435
|
+
await refreshAgentBehavior({
|
|
436
|
+
source: "metadata_invalidation",
|
|
437
|
+
...(version !== undefined ? { metadataVersion: version } : {}),
|
|
438
|
+
memoryRoot,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (!refreshConversation)
|
|
442
|
+
return;
|
|
443
|
+
await refreshConversationDetails(conversationId, {
|
|
444
|
+
source: "metadata_invalidation",
|
|
445
|
+
...(version !== undefined ? { metadataVersion: version } : {}),
|
|
446
|
+
memoryRoot,
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
const syncMessagePathProfiles = async (turn, memoryRoot) => {
|
|
450
|
+
if (turn.senderId === "clawchat-bootstrap") {
|
|
451
|
+
await pullOwnerMetadata({
|
|
452
|
+
memoryRoot,
|
|
453
|
+
agentId: account.agentId,
|
|
454
|
+
accountUserId: account.userId,
|
|
455
|
+
accountOwnerUserId: account.ownerUserId,
|
|
456
|
+
api: getConversationApiClient(),
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (turn.peer.kind === "direct" && (turn.senderId === account.ownerUserId || turn.senderId === account.userId)) {
|
|
461
|
+
await pullOwnerMetadata({
|
|
462
|
+
memoryRoot,
|
|
463
|
+
agentId: account.agentId,
|
|
464
|
+
accountUserId: account.userId,
|
|
465
|
+
accountOwnerUserId: account.ownerUserId,
|
|
466
|
+
api: getConversationApiClient(),
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (turn.peer.kind === "group") {
|
|
471
|
+
await refreshGroupProfile({
|
|
472
|
+
platform: "openclaw",
|
|
473
|
+
accountId,
|
|
474
|
+
conversationId: turn.peer.id,
|
|
475
|
+
api: getConversationApiClient(),
|
|
476
|
+
store: {
|
|
477
|
+
...(store?.upsertConversationDetails ? { upsertConversationDetails: store.upsertConversationDetails.bind(store) } : {}),
|
|
478
|
+
},
|
|
479
|
+
memoryRoot,
|
|
480
|
+
log: { error: (message) => log?.error?.(`[${accountId}] ${message}`) },
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await syncFirstSeenClawChatProfiles({
|
|
485
|
+
platform: "openclaw",
|
|
486
|
+
accountId,
|
|
487
|
+
accountUserId: account.userId,
|
|
488
|
+
accountOwnerUserId: account.ownerUserId,
|
|
489
|
+
chat: {
|
|
490
|
+
id: turn.peer.id,
|
|
491
|
+
type: "direct",
|
|
492
|
+
lastSeenAt: turn.timestamp,
|
|
493
|
+
},
|
|
494
|
+
sender: {
|
|
495
|
+
id: turn.senderId,
|
|
496
|
+
...(turn.senderNickName ? { nickname: turn.senderNickName } : {}),
|
|
497
|
+
},
|
|
498
|
+
api: getConversationApiClient(),
|
|
499
|
+
store: {
|
|
500
|
+
...(store?.getCachedConversation ? { getCachedConversation: store.getCachedConversation.bind(store) } : {}),
|
|
501
|
+
...(store?.upsertConversationSummary ? { upsertConversationSummary: store.upsertConversationSummary.bind(store) } : {}),
|
|
502
|
+
...(store?.upsertConversationDetails ? { upsertConversationDetails: store.upsertConversationDetails.bind(store) } : {}),
|
|
503
|
+
},
|
|
504
|
+
memoryRoot,
|
|
505
|
+
log: { error: (message) => log?.error?.(`[${accountId}] ${message}`) },
|
|
506
|
+
});
|
|
507
|
+
};
|
|
102
508
|
const client = createOpenclawClawlingClient(account, {
|
|
103
509
|
...(params.transport ? { transport: params.transport } : {}),
|
|
104
510
|
wsLifecycle: {
|
|
105
511
|
onConnectFrameSent: (env) => {
|
|
106
512
|
lastConnectTraceId = typeof env.trace_id === "string" ? env.trace_id : "-";
|
|
513
|
+
if (store && currentConnectionId != null) {
|
|
514
|
+
const connectionId = currentConnectionId;
|
|
515
|
+
recordConnection("connect-sent", () => store.markConnectSent(connectionId));
|
|
516
|
+
}
|
|
107
517
|
const deviceId = typeof env.payload?.device_id === "string" ? env.payload.device_id : "-";
|
|
108
518
|
const current = wsLogContext();
|
|
109
519
|
log?.info?.(formatWsLog({
|
|
@@ -123,6 +533,13 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
123
533
|
});
|
|
124
534
|
log?.info?.(`[${accountId}] openclaw-clawchat runtime client created`);
|
|
125
535
|
setAlignedOutboundLogContext(client, wsLogContext);
|
|
536
|
+
client.on("hello:ok", (env) => {
|
|
537
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
538
|
+
? env.payload
|
|
539
|
+
: {};
|
|
540
|
+
lastHelloOkDeviceId = typeof payload.device_id === "string" ? payload.device_id : undefined;
|
|
541
|
+
lastHelloOkDeliveryMode = typeof payload.delivery_mode === "string" ? payload.delivery_mode : undefined;
|
|
542
|
+
});
|
|
126
543
|
const protocolControlLogger = createProtocolControlHandler({
|
|
127
544
|
accountId,
|
|
128
545
|
log: (msg) => log?.info?.(msg),
|
|
@@ -147,6 +564,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
147
564
|
],
|
|
148
565
|
}));
|
|
149
566
|
};
|
|
567
|
+
let dispatchActivationBootstrap = async () => { };
|
|
150
568
|
client.on("state", ({ from, to }) => {
|
|
151
569
|
log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
|
|
152
570
|
wsReady = to === "connected";
|
|
@@ -154,6 +572,18 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
154
572
|
reconnectTracker.connectStart();
|
|
155
573
|
currentAttemptStartedAt = Date.now();
|
|
156
574
|
const current = wsLogContext();
|
|
575
|
+
if (store) {
|
|
576
|
+
recordConnection("start", () => {
|
|
577
|
+
currentConnectionId = store.startConnection({
|
|
578
|
+
platform: "openclaw",
|
|
579
|
+
accountId,
|
|
580
|
+
attempt: current.attempt,
|
|
581
|
+
reconnectCount: current.reconnectCount,
|
|
582
|
+
connectStartedAt: currentAttemptStartedAt,
|
|
583
|
+
});
|
|
584
|
+
currentConnectionFinished = false;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
157
587
|
log?.info?.(formatWsLog({
|
|
158
588
|
event: "connect_start",
|
|
159
589
|
accountId,
|
|
@@ -172,6 +602,19 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
172
602
|
const queueSize = getAlignedOutboundQueueSize(client);
|
|
173
603
|
reconnectTracker.markReady();
|
|
174
604
|
const current = wsLogContext();
|
|
605
|
+
if (store && currentConnectionId != null) {
|
|
606
|
+
const connectionId = currentConnectionId;
|
|
607
|
+
recordConnection("ready", () => {
|
|
608
|
+
if (lastHelloOkDeviceId === undefined && lastHelloOkDeliveryMode === undefined) {
|
|
609
|
+
store.markConnectionReady(connectionId);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
store.markConnectionReady(connectionId, {
|
|
613
|
+
...(lastHelloOkDeviceId !== undefined ? { resolvedDeviceId: lastHelloOkDeviceId } : {}),
|
|
614
|
+
...(lastHelloOkDeliveryMode !== undefined ? { deliveryMode: lastHelloOkDeliveryMode } : {}),
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
175
618
|
log?.info?.(formatWsLog({
|
|
176
619
|
event: "handshake_ok",
|
|
177
620
|
accountId,
|
|
@@ -191,6 +634,8 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
191
634
|
catch {
|
|
192
635
|
// The queue keeps the failed frame at the head and will retry after the next reconnect.
|
|
193
636
|
}
|
|
637
|
+
void refreshConversationCacheAfterReady();
|
|
638
|
+
void dispatchActivationBootstrap();
|
|
194
639
|
}
|
|
195
640
|
else if (to === "disconnected") {
|
|
196
641
|
reconnectTracker.markClosed();
|
|
@@ -201,6 +646,11 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
201
646
|
client.on("close", ({ code, reason }) => {
|
|
202
647
|
if (closingForAbort || (code === 1000 && reason === "client close"))
|
|
203
648
|
return;
|
|
649
|
+
finishCurrentConnection({
|
|
650
|
+
state: "disconnected",
|
|
651
|
+
closeCode: code ?? null,
|
|
652
|
+
closeReason: reason ?? null,
|
|
653
|
+
});
|
|
204
654
|
const current = wsLogContext();
|
|
205
655
|
log?.info?.(formatWsLog({
|
|
206
656
|
event: "connection_lost",
|
|
@@ -315,9 +765,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
315
765
|
const payload = env.payload;
|
|
316
766
|
lastHelloFailReason = typeof payload?.reason === "string" ? payload.reason : "";
|
|
317
767
|
});
|
|
768
|
+
client.on("metadata:invalidated", (env) => {
|
|
769
|
+
void handleMetadataInvalidation(env);
|
|
770
|
+
});
|
|
318
771
|
client.on("error", (err) => {
|
|
319
772
|
const classified = classifyClawlingClientError(err);
|
|
320
773
|
if (classified.kind === "auth") {
|
|
774
|
+
finishCurrentConnection({
|
|
775
|
+
state: "auth_failed",
|
|
776
|
+
error: lastHelloFailReason || classified.message,
|
|
777
|
+
});
|
|
321
778
|
logAuthFailure(classified.message);
|
|
322
779
|
setStatus({
|
|
323
780
|
...getStatus(),
|
|
@@ -328,6 +785,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
328
785
|
});
|
|
329
786
|
}
|
|
330
787
|
else if (classified.kind === "transport") {
|
|
788
|
+
finishCurrentConnection({ state: "transport_error", error: classified.message });
|
|
331
789
|
const current = wsLogContext();
|
|
332
790
|
log?.info?.(formatWsLog({
|
|
333
791
|
event: "connection_lost",
|
|
@@ -354,10 +812,305 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
354
812
|
log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
|
|
355
813
|
}
|
|
356
814
|
else {
|
|
357
|
-
log?.error?.(`[${accountId}] openclaw-clawchat
|
|
815
|
+
log?.error?.(`[${accountId}] openclaw-clawchat client error: ${classified.message}`);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
const buildPromptForTurn = async (turn, memoryRoot) => {
|
|
819
|
+
const senderRelation = resolveSenderRelation({
|
|
820
|
+
senderId: turn.senderId,
|
|
821
|
+
accountUserId: account.userId,
|
|
822
|
+
accountOwnerUserId: account.ownerUserId,
|
|
823
|
+
senderProfileType: turn.senderProfileType,
|
|
824
|
+
});
|
|
825
|
+
const promptChatType = turn.peer.kind === "group" ? "group" : "dm";
|
|
826
|
+
const promptMetadata = await loadClawChatPromptMetadata({
|
|
827
|
+
memoryRoot,
|
|
828
|
+
turn: {
|
|
829
|
+
chatType: promptChatType,
|
|
830
|
+
senderId: turn.senderId,
|
|
831
|
+
senderIsOwner: senderRelation === "owner",
|
|
832
|
+
groupId: turn.peer.kind === "group" ? turn.peer.id : null,
|
|
833
|
+
},
|
|
834
|
+
log: { error: (message) => log?.error?.(`[${accountId}] ${message}`) },
|
|
835
|
+
});
|
|
836
|
+
const metadataSenderProfileType = promptMetadata.userMetadata?.profile_type ?? null;
|
|
837
|
+
const promptSenderProfileType = metadataSenderProfileType ?? turn.senderProfileType ?? (senderRelation === "self_agent" || senderRelation === "peer_agent" ? "agent" : "user");
|
|
838
|
+
return renderClawChatProfilePrompt({
|
|
839
|
+
basePrompt: turn.peer.kind === "group" ? getClawChatGroupPrompt() : getClawChatUserPrompt(),
|
|
840
|
+
...promptMetadata,
|
|
841
|
+
turn: {
|
|
842
|
+
chatType: promptChatType,
|
|
843
|
+
senderId: turn.senderId,
|
|
844
|
+
senderName: promptMetadata.userMetadata?.nickname ?? (turn.senderNickName || turn.senderId),
|
|
845
|
+
senderProfileType: promptSenderProfileType,
|
|
846
|
+
senderIsOwner: senderRelation === "owner",
|
|
847
|
+
groupId: turn.peer.kind === "group" ? turn.peer.id : null,
|
|
848
|
+
coalescedGroupBatch: turn.coalescedGroupBatch === true,
|
|
849
|
+
wasMentioned: turn.wasMentioned,
|
|
850
|
+
mentionedUserIds: turn.mentionedUserIds,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
};
|
|
854
|
+
const resolveSenderNickNameForTurn = (turn) => {
|
|
855
|
+
if (turn.senderNickName && turn.senderNickName !== turn.senderId)
|
|
856
|
+
return turn.senderNickName;
|
|
857
|
+
return turn.senderNickName || turn.senderId;
|
|
858
|
+
};
|
|
859
|
+
const resolveSenderBatchIdentityForTurn = (turn) => {
|
|
860
|
+
const senderRelation = resolveSenderRelation({
|
|
861
|
+
senderId: turn.senderId,
|
|
862
|
+
accountUserId: account.userId,
|
|
863
|
+
accountOwnerUserId: account.ownerUserId,
|
|
864
|
+
senderProfileType: turn.senderProfileType,
|
|
865
|
+
});
|
|
866
|
+
return {
|
|
867
|
+
senderRelation,
|
|
868
|
+
senderProfileType: turn.senderProfileType ??
|
|
869
|
+
(senderRelation === "self_agent" || senderRelation === "peer_agent" ? "agent" : "user"),
|
|
870
|
+
senderIsOwner: senderRelation === "owner",
|
|
871
|
+
};
|
|
872
|
+
};
|
|
873
|
+
const claimInboundTurn = (turn) => {
|
|
874
|
+
const env = turn.envelope;
|
|
875
|
+
if (store?.claimMessageOnce) {
|
|
876
|
+
const claimed = recordConnection("message claim", () => store.claimMessageOnce?.({
|
|
877
|
+
platform: "openclaw",
|
|
878
|
+
accountId,
|
|
879
|
+
kind: "message",
|
|
880
|
+
direction: "inbound",
|
|
881
|
+
eventType: String(env.event),
|
|
882
|
+
traceId: turn.traceId,
|
|
883
|
+
chatId: turn.peer.id,
|
|
884
|
+
messageId: turn.messageId,
|
|
885
|
+
text: turn.rawBody,
|
|
886
|
+
raw: env,
|
|
887
|
+
}));
|
|
888
|
+
if (claimed === false) {
|
|
889
|
+
log?.info?.(`[${accountId}] openclaw-clawchat skip duplicate stored msg=${turn.messageId}`);
|
|
890
|
+
return "skipped";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return "claimed";
|
|
894
|
+
};
|
|
895
|
+
const dispatchTurnToAgent = async (turn) => {
|
|
896
|
+
const rt = runtime.channel;
|
|
897
|
+
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
898
|
+
const routeCfg = withClawChatSessionScope(cfg);
|
|
899
|
+
const route = rt.routing.resolveAgentRoute({
|
|
900
|
+
cfg: routeCfg,
|
|
901
|
+
channel: CHANNEL_ID,
|
|
902
|
+
accountId,
|
|
903
|
+
peer: turn.peer,
|
|
904
|
+
});
|
|
905
|
+
const memoryRoot = resolveClawChatMemoryRoot(runtime, cfg, route.agentId);
|
|
906
|
+
const body = rt.reply.formatAgentEnvelope({
|
|
907
|
+
channel: "Clawling Chat",
|
|
908
|
+
from: formatConversationSubject(turn.peer),
|
|
909
|
+
body: turn.rawBody,
|
|
910
|
+
timestamp: turn.timestamp,
|
|
911
|
+
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
912
|
+
});
|
|
913
|
+
try {
|
|
914
|
+
await syncMessagePathProfiles(turn, memoryRoot);
|
|
915
|
+
}
|
|
916
|
+
catch (err) {
|
|
917
|
+
log?.error?.(`[${accountId}] openclaw-clawchat message metadata refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
918
|
+
}
|
|
919
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
920
|
+
const turnPrompt = await buildPromptForTurn(turn, memoryRoot);
|
|
921
|
+
const ctxPayload = rt.turn.buildContext({
|
|
922
|
+
channel: CHANNEL_ID,
|
|
923
|
+
accountId: route.accountId ?? accountId,
|
|
924
|
+
provider: CHANNEL_ID,
|
|
925
|
+
surface: CHANNEL_ID,
|
|
926
|
+
messageId: turn.messageId,
|
|
927
|
+
messageIdFull: turn.messageId,
|
|
928
|
+
timestamp: turn.timestamp,
|
|
929
|
+
from: conversationTarget,
|
|
930
|
+
sender: {
|
|
931
|
+
id: turn.senderId,
|
|
932
|
+
name: turn.senderNickName || turn.senderId,
|
|
933
|
+
displayLabel: turn.senderNickName || turn.senderId,
|
|
934
|
+
},
|
|
935
|
+
conversation: {
|
|
936
|
+
kind: turn.peer.kind,
|
|
937
|
+
id: turn.peer.id,
|
|
938
|
+
label: formatConversationSubject(turn.peer),
|
|
939
|
+
routePeer: turn.peer,
|
|
940
|
+
},
|
|
941
|
+
route: {
|
|
942
|
+
agentId: route.agentId,
|
|
943
|
+
accountId: route.accountId ?? accountId,
|
|
944
|
+
routeSessionKey: route.sessionKey,
|
|
945
|
+
},
|
|
946
|
+
reply: {
|
|
947
|
+
to: `${CHANNEL_ID}:${account.userId}`,
|
|
948
|
+
originatingTo: conversationTarget,
|
|
949
|
+
},
|
|
950
|
+
message: {
|
|
951
|
+
body,
|
|
952
|
+
rawBody: turn.rawBody,
|
|
953
|
+
bodyForAgent: turn.rawBody,
|
|
954
|
+
commandBody: turn.rawBody,
|
|
955
|
+
envelopeFrom: conversationTarget,
|
|
956
|
+
},
|
|
957
|
+
access: {
|
|
958
|
+
mentions: {
|
|
959
|
+
canDetectMention: true,
|
|
960
|
+
wasMentioned: turn.wasMentioned,
|
|
961
|
+
hasAnyMention: turn.mentionedUserIds.length > 0,
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
...(memoryRoot ? { extra: { memoryRoot } } : {}),
|
|
965
|
+
...(turn.peer.kind === "group"
|
|
966
|
+
? { supplemental: { groupSystemPrompt: turnPrompt } }
|
|
967
|
+
: {}),
|
|
968
|
+
});
|
|
969
|
+
if (memoryRoot) {
|
|
970
|
+
ctxPayload.memoryRoot = memoryRoot;
|
|
971
|
+
}
|
|
972
|
+
if (turn.mentionedUserIds.length > 0) {
|
|
973
|
+
ctxPayload.MentionedUserIds = turn.mentionedUserIds;
|
|
974
|
+
}
|
|
975
|
+
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
976
|
+
const inboundPaths = turn.mediaItems.length > 0
|
|
977
|
+
? await fetchInboundMedia(turn.mediaItems, {
|
|
978
|
+
runtime,
|
|
979
|
+
log,
|
|
980
|
+
maxBytes: 20 * 1024 * 1024,
|
|
981
|
+
})
|
|
982
|
+
: [];
|
|
983
|
+
if (inboundPaths.length > 0) {
|
|
984
|
+
ctxPayload.MediaPath = inboundPaths[0];
|
|
985
|
+
ctxPayload.MediaPaths = inboundPaths;
|
|
986
|
+
}
|
|
987
|
+
const resolvedSessionKey = ctxPayload.SessionKey ?? route.sessionKey;
|
|
988
|
+
clearClawChatPromptInjectionForSession(resolvedSessionKey);
|
|
989
|
+
const stagedDirectPrompt = turn.peer.kind !== "group";
|
|
990
|
+
if (stagedDirectPrompt) {
|
|
991
|
+
stageClawChatPromptInjection({
|
|
992
|
+
sessionKey: resolvedSessionKey,
|
|
993
|
+
prompt: turnPrompt,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
try {
|
|
998
|
+
await rt.session.recordInboundSession({
|
|
999
|
+
storePath,
|
|
1000
|
+
sessionKey: resolvedSessionKey,
|
|
1001
|
+
ctx: ctxPayload,
|
|
1002
|
+
onRecordError: (err) => {
|
|
1003
|
+
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
catch (err) {
|
|
1008
|
+
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
1009
|
+
}
|
|
1010
|
+
const replyCtx = turn.replyCtx;
|
|
1011
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
|
|
1012
|
+
cfg,
|
|
1013
|
+
runtime,
|
|
1014
|
+
account,
|
|
1015
|
+
client,
|
|
1016
|
+
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
1017
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
1018
|
+
inboundMessageId: turn.messageId,
|
|
1019
|
+
inboundForFinalReply: {
|
|
1020
|
+
chatId: turn.peer.id,
|
|
1021
|
+
senderId: turn.senderId,
|
|
1022
|
+
senderNickName: turn.senderNickName || turn.senderId,
|
|
1023
|
+
bodyText: turn.rawBody,
|
|
1024
|
+
},
|
|
1025
|
+
store: store
|
|
1026
|
+
? {
|
|
1027
|
+
insertMessage: (input) => store.insertMessage?.(input) ?? null,
|
|
1028
|
+
claimMessageOnce: (input) => store.claimMessageOnce?.(input) ?? null,
|
|
1029
|
+
updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
|
|
1030
|
+
}
|
|
1031
|
+
: null,
|
|
1032
|
+
log,
|
|
1033
|
+
});
|
|
1034
|
+
const agentsConfigured = Object.keys(cfg.agents ?? {});
|
|
1035
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${resolvedSessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`);
|
|
1036
|
+
try {
|
|
1037
|
+
const dispatchResult = await rt.reply.withReplyDispatcher({
|
|
1038
|
+
dispatcher,
|
|
1039
|
+
onSettled: () => markDispatchIdle(),
|
|
1040
|
+
run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
|
|
1041
|
+
});
|
|
1042
|
+
const counts = dispatchResult?.counts ?? {};
|
|
1043
|
+
const queuedFinal = Boolean(dispatchResult?.queuedFinal);
|
|
1044
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
|
|
1045
|
+
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
1046
|
+
log?.info?.(`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
|
|
1047
|
+
`Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
|
|
1048
|
+
`or send-policy denied; or a plugin claimed the binding.`);
|
|
1049
|
+
}
|
|
1050
|
+
return "submitted";
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
log?.error?.(`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`);
|
|
1054
|
+
return "failed";
|
|
1055
|
+
}
|
|
358
1056
|
}
|
|
1057
|
+
finally {
|
|
1058
|
+
if (stagedDirectPrompt) {
|
|
1059
|
+
clearClawChatPromptInjectionForSession(resolvedSessionKey);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
const groupCoalescer = createGroupMessageCoalescer({
|
|
1064
|
+
idleMs: 10_000,
|
|
1065
|
+
maxWaitMs: 30_000,
|
|
1066
|
+
dispatch: async (turn) => {
|
|
1067
|
+
await dispatchTurnToAgent(turn);
|
|
1068
|
+
},
|
|
1069
|
+
onError: (err) => {
|
|
1070
|
+
log?.error?.(`[${accountId}] openclaw-clawchat coalesced group dispatch failed: ${String(err)}`);
|
|
1071
|
+
},
|
|
1072
|
+
onDrop: (chatId, count) => {
|
|
1073
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dropped pending group batch chat_id=${chatId} count=${count} reason=shutdown`);
|
|
1074
|
+
},
|
|
359
1075
|
});
|
|
360
|
-
|
|
1076
|
+
const ingestTurn = async (rawTurn) => {
|
|
1077
|
+
const senderBatchIdentity = rawTurn.peer.kind === "group"
|
|
1078
|
+
? resolveSenderBatchIdentityForTurn(rawTurn)
|
|
1079
|
+
: {};
|
|
1080
|
+
const turn = {
|
|
1081
|
+
...rawTurn,
|
|
1082
|
+
senderNickName: resolveSenderNickNameForTurn(rawTurn),
|
|
1083
|
+
...senderBatchIdentity,
|
|
1084
|
+
};
|
|
1085
|
+
const claimed = claimInboundTurn(turn);
|
|
1086
|
+
if (claimed === "skipped")
|
|
1087
|
+
return "skipped";
|
|
1088
|
+
if (turn.peer.kind === "group") {
|
|
1089
|
+
if (isKnownOpenClawGroupSlashCommand(turn.rawBody, cfg)) {
|
|
1090
|
+
const commandMode = effectiveGroupCommandMode(account, turn.peer.id);
|
|
1091
|
+
const commandAllowed = commandMode === "all"
|
|
1092
|
+
|| (commandMode === "owner" && turn.senderIsOwner === true);
|
|
1093
|
+
if (!commandAllowed) {
|
|
1094
|
+
log?.info?.(`[${accountId}] openclaw-clawchat group command dropped chat_id=${turn.peer.id} msg=${turn.messageId} mode=${commandMode} owner=${turn.senderIsOwner === true}`);
|
|
1095
|
+
return "skipped";
|
|
1096
|
+
}
|
|
1097
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dispatching group command chat_id=${turn.peer.id} msg=${turn.messageId} mode=${commandMode}`);
|
|
1098
|
+
return dispatchTurnToAgent(turn);
|
|
1099
|
+
}
|
|
1100
|
+
if (turn.wasMentioned) {
|
|
1101
|
+
groupCoalescer.enqueue(turn);
|
|
1102
|
+
groupCoalescer.flushNow(turn.peer.id);
|
|
1103
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dispatching mentioned group batch chat_id=${turn.peer.id} msg=${turn.messageId}`);
|
|
1104
|
+
return "submitted";
|
|
1105
|
+
}
|
|
1106
|
+
groupCoalescer.enqueue(turn);
|
|
1107
|
+
log?.info?.(`[${accountId}] openclaw-clawchat queued group batch chat_id=${turn.peer.id} msg=${turn.messageId}`);
|
|
1108
|
+
return "submitted";
|
|
1109
|
+
}
|
|
1110
|
+
return dispatchTurnToAgent(turn);
|
|
1111
|
+
};
|
|
1112
|
+
const handleInboundEnvelope = async (env) => {
|
|
1113
|
+
let ingestResult;
|
|
361
1114
|
try {
|
|
362
1115
|
await dispatchOpenclawClawlingInbound({
|
|
363
1116
|
envelope: env,
|
|
@@ -366,119 +1119,60 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
366
1119
|
account,
|
|
367
1120
|
log,
|
|
368
1121
|
ingest: async (turn) => {
|
|
369
|
-
|
|
370
|
-
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
371
|
-
const routeCfg = withClawChatSessionScope(cfg);
|
|
372
|
-
const route = rt.routing.resolveAgentRoute({
|
|
373
|
-
cfg: routeCfg,
|
|
374
|
-
channel: CHANNEL_ID,
|
|
375
|
-
accountId,
|
|
376
|
-
peer: turn.peer,
|
|
377
|
-
});
|
|
378
|
-
const body = rt.reply.formatAgentEnvelope({
|
|
379
|
-
channel: "Clawling Chat",
|
|
380
|
-
from: formatConversationSubject(turn.peer),
|
|
381
|
-
body: turn.rawBody,
|
|
382
|
-
timestamp: turn.timestamp,
|
|
383
|
-
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
384
|
-
});
|
|
385
|
-
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
386
|
-
const ctxPayload = rt.reply.finalizeInboundContext({
|
|
387
|
-
Body: body,
|
|
388
|
-
BodyForAgent: turn.rawBody,
|
|
389
|
-
RawBody: turn.rawBody,
|
|
390
|
-
CommandBody: turn.rawBody,
|
|
391
|
-
// Clawling v2 routes by chat_id. `OriginatingTo` is what the
|
|
392
|
-
// message tool uses as the implicit current-chat target, so keep it
|
|
393
|
-
// on the conversation id rather than the agent account user id.
|
|
394
|
-
From: conversationTarget,
|
|
395
|
-
To: `${CHANNEL_ID}:${account.userId}`,
|
|
396
|
-
SessionKey: route.sessionKey,
|
|
397
|
-
AccountId: route.accountId ?? accountId,
|
|
398
|
-
ChatType: turn.peer.kind,
|
|
399
|
-
ConversationLabel: formatConversationSubject(turn.peer),
|
|
400
|
-
SenderId: turn.senderId,
|
|
401
|
-
Provider: CHANNEL_ID,
|
|
402
|
-
Surface: CHANNEL_ID,
|
|
403
|
-
MessageSid: turn.messageId,
|
|
404
|
-
MessageSidFull: turn.messageId,
|
|
405
|
-
Timestamp: turn.timestamp,
|
|
406
|
-
OriginatingChannel: CHANNEL_ID,
|
|
407
|
-
OriginatingTo: conversationTarget,
|
|
408
|
-
});
|
|
409
|
-
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
410
|
-
const inboundPaths = turn.mediaItems.length > 0
|
|
411
|
-
? await fetchInboundMedia(turn.mediaItems, {
|
|
412
|
-
runtime,
|
|
413
|
-
log,
|
|
414
|
-
maxBytes: 20 * 1024 * 1024,
|
|
415
|
-
})
|
|
416
|
-
: [];
|
|
417
|
-
if (inboundPaths.length > 0) {
|
|
418
|
-
ctxPayload.MediaPath = inboundPaths[0];
|
|
419
|
-
ctxPayload.MediaPaths = inboundPaths;
|
|
420
|
-
}
|
|
421
|
-
try {
|
|
422
|
-
await rt.session.recordInboundSession({
|
|
423
|
-
storePath,
|
|
424
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
425
|
-
ctx: ctxPayload,
|
|
426
|
-
onRecordError: (err) => {
|
|
427
|
-
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
428
|
-
},
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
catch (err) {
|
|
432
|
-
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
433
|
-
}
|
|
434
|
-
const replyCtx = turn.replyCtx;
|
|
435
|
-
const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
|
|
436
|
-
cfg,
|
|
437
|
-
runtime,
|
|
438
|
-
account,
|
|
439
|
-
client,
|
|
440
|
-
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
441
|
-
...(replyCtx ? { replyCtx } : {}),
|
|
442
|
-
inboundMessageId: turn.messageId,
|
|
443
|
-
inboundForFinalReply: {
|
|
444
|
-
chatId: turn.peer.id,
|
|
445
|
-
senderId: turn.senderId,
|
|
446
|
-
senderNickName: turn.senderNickName || turn.senderId,
|
|
447
|
-
bodyText: turn.rawBody,
|
|
448
|
-
},
|
|
449
|
-
log,
|
|
450
|
-
});
|
|
451
|
-
const agentsConfigured = Object.keys(cfg.agents ?? {});
|
|
452
|
-
log?.info?.(`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${ctxPayload.SessionKey ?? route.sessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`);
|
|
453
|
-
try {
|
|
454
|
-
const dispatchResult = await rt.reply.withReplyDispatcher({
|
|
455
|
-
dispatcher,
|
|
456
|
-
onSettled: () => markDispatchIdle(),
|
|
457
|
-
run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
|
|
458
|
-
});
|
|
459
|
-
const counts = dispatchResult?.counts ?? {};
|
|
460
|
-
const queuedFinal = Boolean(dispatchResult?.queuedFinal);
|
|
461
|
-
log?.info?.(`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
|
|
462
|
-
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
463
|
-
log?.info?.(`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
|
|
464
|
-
`Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
|
|
465
|
-
`or send-policy denied; or a plugin claimed the binding.`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
catch (err) {
|
|
469
|
-
log?.error?.(`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`);
|
|
470
|
-
}
|
|
1122
|
+
ingestResult = await ingestTurn(turn);
|
|
471
1123
|
},
|
|
472
1124
|
});
|
|
473
1125
|
}
|
|
474
1126
|
catch (err) {
|
|
475
1127
|
log?.error?.(`[${accountId}] openclaw-clawchat message handler error: ${err instanceof Error ? err.stack || err.message : String(err)}`);
|
|
1128
|
+
return "failed";
|
|
476
1129
|
}
|
|
1130
|
+
return ingestResult;
|
|
1131
|
+
};
|
|
1132
|
+
dispatchActivationBootstrap = async () => {
|
|
1133
|
+
if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent)
|
|
1134
|
+
return;
|
|
1135
|
+
let bootstrap;
|
|
1136
|
+
const releaseBootstrap = () => {
|
|
1137
|
+
if (!bootstrap || !store.releaseActivationBootstrapClaim)
|
|
1138
|
+
return;
|
|
1139
|
+
const claimedBootstrap = bootstrap;
|
|
1140
|
+
recordConnection("activation bootstrap release", () => store.releaseActivationBootstrapClaim?.({
|
|
1141
|
+
platform: "openclaw",
|
|
1142
|
+
accountId,
|
|
1143
|
+
conversationId: claimedBootstrap.conversationId,
|
|
1144
|
+
}));
|
|
1145
|
+
};
|
|
1146
|
+
try {
|
|
1147
|
+
bootstrap = recordConnection("activation bootstrap claim", () => store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }));
|
|
1148
|
+
if (!bootstrap)
|
|
1149
|
+
return;
|
|
1150
|
+
const claimedBootstrap = bootstrap;
|
|
1151
|
+
const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
|
|
1152
|
+
if (result !== "submitted") {
|
|
1153
|
+
releaseBootstrap();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
|
|
1157
|
+
platform: "openclaw",
|
|
1158
|
+
accountId,
|
|
1159
|
+
conversationId: claimedBootstrap.conversationId,
|
|
1160
|
+
}));
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
releaseBootstrap();
|
|
1164
|
+
log?.error?.(`[${accountId}] openclaw-clawchat activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
client.on("message", (env) => {
|
|
1168
|
+
void (async () => {
|
|
1169
|
+
await handleInboundEnvelope(env);
|
|
1170
|
+
})();
|
|
477
1171
|
});
|
|
478
1172
|
// `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
|
|
479
1173
|
// (auth). Transport failures (server unreachable, DNS error, etc.) do
|
|
480
|
-
// NOT reject this promise — the
|
|
481
|
-
// its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
1174
|
+
// NOT reject this promise — the local client handles them internally and
|
|
1175
|
+
// drives its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
482
1176
|
// capped at `maxDelay`, with jitter). So we never throw here on anything
|
|
483
1177
|
// other than auth failure; on auth we tear the account down cleanly and
|
|
484
1178
|
// return without throwing (which would make the gateway supervisor
|
|
@@ -498,6 +1192,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
498
1192
|
lastError: classified.message,
|
|
499
1193
|
});
|
|
500
1194
|
if (classified.kind === "auth") {
|
|
1195
|
+
finishCurrentConnection({
|
|
1196
|
+
state: "auth_failed",
|
|
1197
|
+
error: lastHelloFailReason || classified.message,
|
|
1198
|
+
});
|
|
501
1199
|
logAuthFailure(classified.message);
|
|
502
1200
|
return;
|
|
503
1201
|
}
|
|
@@ -517,6 +1215,12 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
517
1215
|
log?.info?.(`[${accountId}] openclaw-clawchat runtime abort received; closing client`);
|
|
518
1216
|
activeClients.delete(accountId);
|
|
519
1217
|
closingForAbort = true;
|
|
1218
|
+
groupCoalescer.cancelAll();
|
|
1219
|
+
finishCurrentConnection({
|
|
1220
|
+
state: "disconnected",
|
|
1221
|
+
closeCode: 1000,
|
|
1222
|
+
closeReason: "client close",
|
|
1223
|
+
});
|
|
520
1224
|
client.close();
|
|
521
1225
|
setStatus({
|
|
522
1226
|
...getStatus(),
|