@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
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/INSTALL.md +64 -0
- package/README.md +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- 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 +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -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.test.ts +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
package/src/runtime.ts
CHANGED
|
@@ -4,24 +4,58 @@ import {
|
|
|
4
4
|
ProtocolError,
|
|
5
5
|
StateError,
|
|
6
6
|
TransportError,
|
|
7
|
-
|
|
8
|
-
type DownlinkMessageSendPayload,
|
|
7
|
+
EVENT,
|
|
9
8
|
type Envelope,
|
|
10
9
|
type Transport,
|
|
11
|
-
} from "
|
|
10
|
+
} from "./protocol-types.ts";
|
|
12
11
|
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
|
|
13
12
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
14
13
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
15
14
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
16
15
|
import { createOpenclawClawlingClient } from "./client.ts";
|
|
16
|
+
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
17
|
+
import { ClawlingApiError, type ConversationDetails, type ConversationParticipant } from "./api-types.ts";
|
|
17
18
|
import { CHANNEL_ID, type ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
18
|
-
import {
|
|
19
|
+
import type { ClawlingChatClient } from "./ws-client.ts";
|
|
20
|
+
import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
|
|
19
21
|
import { fetchInboundMedia } from "./media-runtime.ts";
|
|
20
22
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
|
|
21
23
|
import { sendStreamingText } from "./streaming.ts";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
flushAlignedOutboundQueue,
|
|
26
|
+
getAlignedOutboundQueueSize,
|
|
27
|
+
setAlignedOutboundLogContext,
|
|
28
|
+
} from "./outbound.ts";
|
|
29
|
+
import { formatWsLog } from "./ws-log.ts";
|
|
30
|
+
import { createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.ts";
|
|
31
|
+
import {
|
|
32
|
+
clawChatDbPathForStateDir,
|
|
33
|
+
getClawChatStore,
|
|
34
|
+
type ClawChatStore,
|
|
35
|
+
} from "./storage.ts";
|
|
23
36
|
|
|
24
37
|
type Log = { info?: (m: string) => void; error?: (m: string) => void };
|
|
38
|
+
type RuntimeConnectionStore = Pick<
|
|
39
|
+
ClawChatStore,
|
|
40
|
+
"startConnection" | "markConnectSent" | "markConnectionReady" | "finishConnection"
|
|
41
|
+
> &
|
|
42
|
+
Partial<
|
|
43
|
+
Pick<
|
|
44
|
+
ClawChatStore,
|
|
45
|
+
| "insertMessage"
|
|
46
|
+
| "claimMessageOnce"
|
|
47
|
+
| "updateMessageByIdentity"
|
|
48
|
+
| "claimPendingActivationBootstrap"
|
|
49
|
+
| "releaseActivationBootstrapClaim"
|
|
50
|
+
| "markActivationBootstrapSent"
|
|
51
|
+
| "upsertConversationSummary"
|
|
52
|
+
| "upsertConversationDetails"
|
|
53
|
+
| "deleteConversationCache"
|
|
54
|
+
| "listCachedConversationIds"
|
|
55
|
+
| "getActivationConversation"
|
|
56
|
+
| "getCachedConversation"
|
|
57
|
+
>
|
|
58
|
+
>;
|
|
25
59
|
|
|
26
60
|
const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } =
|
|
27
61
|
createPluginRuntimeStore<PluginRuntime>("openclaw-clawchat runtime not initialized");
|
|
@@ -107,6 +141,109 @@ function formatConversationSubject(peer: { kind: "direct" | "group"; id: string
|
|
|
107
141
|
return peer.kind === "group" ? `group:${peer.id}` : peer.id;
|
|
108
142
|
}
|
|
109
143
|
|
|
144
|
+
function parseApiTimestamp(value: unknown): number | null {
|
|
145
|
+
if (typeof value !== "string") return null;
|
|
146
|
+
const parsed = Date.parse(value);
|
|
147
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
151
|
+
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function optionalString(value: unknown): string | undefined {
|
|
155
|
+
return typeof value === "string" ? value : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isConversationNotFoundError(err: unknown): boolean {
|
|
159
|
+
if (!(err instanceof ClawlingApiError)) return false;
|
|
160
|
+
return err.meta?.status === 404 || err.meta?.status === 410 ||
|
|
161
|
+
err.meta?.code === 404 || err.meta?.code === 410 || err.meta?.code === 40401;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function metadataVersionFromEnvelope(env: Envelope): number | undefined {
|
|
165
|
+
const payload = asRecord(env.payload);
|
|
166
|
+
const version = payload?.version;
|
|
167
|
+
return typeof version === "number" && Number.isFinite(version) ? version : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function conversationUserProfileFromParticipant(
|
|
171
|
+
participant: ConversationParticipant,
|
|
172
|
+
refreshedAt: number,
|
|
173
|
+
): {
|
|
174
|
+
userId: string;
|
|
175
|
+
nickname?: string | null;
|
|
176
|
+
avatarUrl?: string | null;
|
|
177
|
+
bio?: string | null;
|
|
178
|
+
raw?: unknown;
|
|
179
|
+
lastRefreshedAt?: number | null;
|
|
180
|
+
} | null {
|
|
181
|
+
const participantRecord = participant as Record<string, unknown>;
|
|
182
|
+
const userRecord = asRecord(participantRecord.user);
|
|
183
|
+
const source = userRecord ?? participantRecord;
|
|
184
|
+
const userId = optionalString(source.id) ?? optionalString(participantRecord.user_id);
|
|
185
|
+
if (!userId) return null;
|
|
186
|
+
const profile: {
|
|
187
|
+
userId: string;
|
|
188
|
+
nickname?: string | null;
|
|
189
|
+
avatarUrl?: string | null;
|
|
190
|
+
bio?: string | null;
|
|
191
|
+
raw?: unknown;
|
|
192
|
+
lastRefreshedAt?: number | null;
|
|
193
|
+
} = { userId, raw: source, lastRefreshedAt: refreshedAt };
|
|
194
|
+
const nickname = optionalString(source.nickname);
|
|
195
|
+
const avatarUrl = optionalString(source.avatar_url);
|
|
196
|
+
const bio = optionalString(source.bio);
|
|
197
|
+
if (nickname !== undefined) profile.nickname = nickname;
|
|
198
|
+
if (avatarUrl !== undefined) profile.avatarUrl = avatarUrl;
|
|
199
|
+
if (bio !== undefined) profile.bio = bio;
|
|
200
|
+
return profile;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildConversationDetailsCacheInput(params: {
|
|
204
|
+
accountId: string;
|
|
205
|
+
conversation: ConversationDetails;
|
|
206
|
+
metadataVersion?: number;
|
|
207
|
+
}): Parameters<NonNullable<RuntimeConnectionStore["upsertConversationDetails"]>>[0] {
|
|
208
|
+
const { accountId, conversation, metadataVersion } = params;
|
|
209
|
+
const refreshedAt = Date.now();
|
|
210
|
+
const rawConversation = conversation as Record<string, unknown>;
|
|
211
|
+
const participants = Array.isArray(conversation.participants) ? conversation.participants : [];
|
|
212
|
+
return {
|
|
213
|
+
platform: "openclaw",
|
|
214
|
+
accountId,
|
|
215
|
+
conversationId: conversation.id,
|
|
216
|
+
conversationType: conversation.type,
|
|
217
|
+
...(metadataVersion !== undefined ? { metadataVersion } : {}),
|
|
218
|
+
lastSeenAt: parseApiTimestamp(conversation.updated_at),
|
|
219
|
+
lastRefreshedAt: refreshedAt,
|
|
220
|
+
raw: conversation,
|
|
221
|
+
...(conversation.type === "group"
|
|
222
|
+
? {
|
|
223
|
+
groupProfile: {
|
|
224
|
+
title: conversation.title,
|
|
225
|
+
...(optionalString(rawConversation.description) !== undefined
|
|
226
|
+
? { description: optionalString(rawConversation.description) }
|
|
227
|
+
: {}),
|
|
228
|
+
...(metadataVersion !== undefined ? { metadataVersion } : {}),
|
|
229
|
+
raw: conversation,
|
|
230
|
+
lastRefreshedAt: refreshedAt,
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
234
|
+
userProfiles: participants
|
|
235
|
+
.map((participant) => conversationUserProfileFromParticipant(participant, refreshedAt))
|
|
236
|
+
.filter((profile): profile is NonNullable<typeof profile> => profile !== null),
|
|
237
|
+
members: participants.map((participant) => ({
|
|
238
|
+
userId: participant.user_id,
|
|
239
|
+
role: participant.role,
|
|
240
|
+
raw: participant,
|
|
241
|
+
lastSeenAt: parseApiTimestamp(participant.joined_at),
|
|
242
|
+
})),
|
|
243
|
+
membersComplete: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
110
247
|
function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
111
248
|
return {
|
|
112
249
|
...cfg,
|
|
@@ -117,6 +254,65 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
|
117
254
|
};
|
|
118
255
|
}
|
|
119
256
|
|
|
257
|
+
function buildActivationBootstrapText(): string {
|
|
258
|
+
return [
|
|
259
|
+
"ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
|
|
260
|
+
"Please do both:",
|
|
261
|
+
"1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
|
|
262
|
+
"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.",
|
|
263
|
+
"Do not ask the user for profile information just for this bootstrap.",
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const CLAWCHAT_GROUP_SYSTEM_PROMPT = [
|
|
268
|
+
"When replying in a ClawChat group, treat the conversation as a shared space. Stay concise, respect the group topic, and avoid exposing hidden runtime context.",
|
|
269
|
+
"",
|
|
270
|
+
"**给 Agents**",
|
|
271
|
+
"你是群组里的伙伴。请理解我们在做的事,理解每一位成员。",
|
|
272
|
+
"也请通过群慢慢和每个人建立关系——你是我们的一员。",
|
|
273
|
+
"",
|
|
274
|
+
"**两条底线**",
|
|
275
|
+
"1. 被 @ 时必须回复",
|
|
276
|
+
"2. Agent 之间连续对话不超过 10 轮,除非有人类伙伴明确允许",
|
|
277
|
+
].join("\n");
|
|
278
|
+
|
|
279
|
+
function buildActivationBootstrapEnvelope(params: {
|
|
280
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
281
|
+
conversationId: string;
|
|
282
|
+
}): Envelope {
|
|
283
|
+
const text = buildActivationBootstrapText();
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
return {
|
|
286
|
+
version: "2",
|
|
287
|
+
event: EVENT.MESSAGE_SEND,
|
|
288
|
+
trace_id: `openclaw-clawchat-bootstrap-${now}`,
|
|
289
|
+
emitted_at: now,
|
|
290
|
+
chat_id: params.conversationId,
|
|
291
|
+
chat_type: "direct",
|
|
292
|
+
to: { id: params.account.userId, type: "direct" },
|
|
293
|
+
sender: {
|
|
294
|
+
id: "clawchat-bootstrap",
|
|
295
|
+
type: "direct",
|
|
296
|
+
nick_name: "ClawChat Activation",
|
|
297
|
+
},
|
|
298
|
+
payload: {
|
|
299
|
+
message_id: `openclaw-clawchat-bootstrap-${params.conversationId}-${now}`,
|
|
300
|
+
message_mode: "normal",
|
|
301
|
+
message: {
|
|
302
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
303
|
+
context: { mentions: [], reply: null },
|
|
304
|
+
streaming: {
|
|
305
|
+
status: "static",
|
|
306
|
+
sequence: 0,
|
|
307
|
+
mutation_policy: "sealed",
|
|
308
|
+
started_at: null,
|
|
309
|
+
completed_at: null,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
120
316
|
export interface StartGatewayParams {
|
|
121
317
|
cfg: OpenClawConfig;
|
|
122
318
|
account: ResolvedOpenclawClawlingAccount;
|
|
@@ -125,29 +321,493 @@ export interface StartGatewayParams {
|
|
|
125
321
|
getStatus: () => ChannelAccountSnapshot;
|
|
126
322
|
log?: Log;
|
|
127
323
|
/** Test hook only. */
|
|
324
|
+
store?: RuntimeConnectionStore | null;
|
|
325
|
+
/** Test hook only. */
|
|
128
326
|
transport?: Transport;
|
|
129
327
|
}
|
|
130
328
|
|
|
329
|
+
function resolveConnectionStore(
|
|
330
|
+
params: StartGatewayParams,
|
|
331
|
+
runtime: PluginRuntime,
|
|
332
|
+
): RuntimeConnectionStore | null {
|
|
333
|
+
if (params.store !== undefined) return params.store;
|
|
334
|
+
if (params.transport) return null;
|
|
335
|
+
try {
|
|
336
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
337
|
+
return getClawChatStore({
|
|
338
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
339
|
+
log: { error: (message) => params.log?.error?.(message) },
|
|
340
|
+
});
|
|
341
|
+
} catch {
|
|
342
|
+
params.log?.error?.("openclaw-clawchat sqlite connection persistence unavailable; continuing.");
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
131
347
|
export async function startOpenclawClawlingGateway(params: StartGatewayParams): Promise<void> {
|
|
132
348
|
const { cfg, account, abortSignal, setStatus, getStatus, log } = params;
|
|
133
349
|
// Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
|
|
134
350
|
const runtime = getOpenclawClawlingRuntime();
|
|
135
351
|
const accountId = account.accountId;
|
|
352
|
+
const store = resolveConnectionStore(params, runtime);
|
|
353
|
+
let conversationApiClient: ReturnType<typeof createOpenclawClawlingApiClient> | undefined;
|
|
354
|
+
const getConversationApiClient = () => {
|
|
355
|
+
conversationApiClient ??= createOpenclawClawlingApiClient({
|
|
356
|
+
baseUrl: account.baseUrl,
|
|
357
|
+
token: account.token,
|
|
358
|
+
userId: account.userId,
|
|
359
|
+
});
|
|
360
|
+
return conversationApiClient;
|
|
361
|
+
};
|
|
136
362
|
|
|
363
|
+
log?.info?.(
|
|
364
|
+
`[${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)"}`,
|
|
365
|
+
);
|
|
366
|
+
let lastHelloFailTraceId = "-";
|
|
367
|
+
let lastHelloFailReason = "";
|
|
368
|
+
let lastConnectTraceId = "-";
|
|
369
|
+
let lastHelloOkDeviceId: string | undefined;
|
|
370
|
+
let lastHelloOkDeliveryMode: string | undefined;
|
|
371
|
+
let currentAttemptStartedAt = 0;
|
|
372
|
+
let authFailureLogged = false;
|
|
373
|
+
let closingForAbort = false;
|
|
374
|
+
let wsReady = false;
|
|
375
|
+
let currentConnectionId: number | null = null;
|
|
376
|
+
let currentConnectionFinished = false;
|
|
377
|
+
const reconnectTracker = createReconnectTracker({
|
|
378
|
+
accountId,
|
|
379
|
+
log: (msg) => log?.info?.(msg),
|
|
380
|
+
maxDelayMs: account.reconnect.maxDelay,
|
|
381
|
+
});
|
|
382
|
+
const wsLogContext = () => {
|
|
383
|
+
const snapshot = reconnectTracker.snapshot();
|
|
384
|
+
return {
|
|
385
|
+
attempt: snapshot.attempt || 1,
|
|
386
|
+
reconnectCount: snapshot.reconnectCount,
|
|
387
|
+
state: snapshot.state === "connected" ? "ready" : snapshot.state,
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
const recordConnection = <T>(action: string, fn: () => T): T | undefined => {
|
|
391
|
+
try {
|
|
392
|
+
return fn();
|
|
393
|
+
} catch {
|
|
394
|
+
log?.error?.(`[${accountId}] openclaw-clawchat sqlite ${action} failed; continuing`);
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const finishCurrentConnection = (input: {
|
|
399
|
+
state: string;
|
|
400
|
+
disconnectedAt?: number;
|
|
401
|
+
closeCode?: number | null;
|
|
402
|
+
closeReason?: string | null;
|
|
403
|
+
error?: string | null;
|
|
404
|
+
}) => {
|
|
405
|
+
if (!store || currentConnectionId == null || currentConnectionFinished) return;
|
|
406
|
+
const connectionId = currentConnectionId;
|
|
407
|
+
recordConnection("finish", () => store.finishConnection(connectionId, input));
|
|
408
|
+
currentConnectionFinished = true;
|
|
409
|
+
currentConnectionId = null;
|
|
410
|
+
};
|
|
411
|
+
const refreshConversationDetails = async (
|
|
412
|
+
conversationId: string,
|
|
413
|
+
options: { metadataVersion?: number; source: string },
|
|
414
|
+
): Promise<void> => {
|
|
415
|
+
try {
|
|
416
|
+
const data = await getConversationApiClient().getConversation(conversationId);
|
|
417
|
+
if (!store?.upsertConversationDetails) return;
|
|
418
|
+
recordConnection("conversation details upsert", () =>
|
|
419
|
+
store.upsertConversationDetails?.(buildConversationDetailsCacheInput({
|
|
420
|
+
accountId,
|
|
421
|
+
conversation: data.conversation,
|
|
422
|
+
...(options.metadataVersion !== undefined ? { metadataVersion: options.metadataVersion } : {}),
|
|
423
|
+
})),
|
|
424
|
+
);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (isConversationNotFoundError(err)) {
|
|
427
|
+
if (store?.deleteConversationCache) {
|
|
428
|
+
recordConnection("conversation cache delete", () =>
|
|
429
|
+
store.deleteConversationCache?.({
|
|
430
|
+
platform: "openclaw",
|
|
431
|
+
accountId,
|
|
432
|
+
conversationId,
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
log?.error?.(
|
|
439
|
+
`[${accountId}] openclaw-clawchat metadata refresh failed source=${options.source} conversation=${conversationId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const refreshConversationCacheAfterReady = async (): Promise<void> => {
|
|
444
|
+
if (!store) return;
|
|
445
|
+
const ids: string[] = [];
|
|
446
|
+
const seen = new Set<string>();
|
|
447
|
+
const addId = (id: unknown) => {
|
|
448
|
+
if (typeof id !== "string" || !id || seen.has(id)) return;
|
|
449
|
+
seen.add(id);
|
|
450
|
+
ids.push(id);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const activation = store.getActivationConversation
|
|
454
|
+
? recordConnection("activation conversation read", () =>
|
|
455
|
+
store.getActivationConversation?.({ platform: "openclaw", accountId }),
|
|
456
|
+
)
|
|
457
|
+
: null;
|
|
458
|
+
addId(activation?.conversationId);
|
|
459
|
+
|
|
460
|
+
const cachedIds = store.listCachedConversationIds
|
|
461
|
+
? recordConnection("cached conversation ids read", () =>
|
|
462
|
+
store.listCachedConversationIds?.({ platform: "openclaw", accountId, limit: 20 }),
|
|
463
|
+
) ?? []
|
|
464
|
+
: [];
|
|
465
|
+
for (const id of cachedIds.slice(0, 20)) addId(id);
|
|
466
|
+
|
|
467
|
+
for (const id of ids) {
|
|
468
|
+
await refreshConversationDetails(id, { source: "reconnect" });
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const handleMetadataInvalidation = async (env: Envelope): Promise<void> => {
|
|
472
|
+
const conversationId = typeof env.chat_id === "string" && env.chat_id.trim()
|
|
473
|
+
? env.chat_id
|
|
474
|
+
: "";
|
|
475
|
+
if (!conversationId) {
|
|
476
|
+
log?.info?.(`[${accountId}] openclaw-clawchat metadata invalidation missing chat_id trace=${env.trace_id}`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const version = metadataVersionFromEnvelope(env);
|
|
481
|
+
if (version !== undefined && store?.getCachedConversation) {
|
|
482
|
+
const cached = recordConnection("conversation cache read", () =>
|
|
483
|
+
store.getCachedConversation?.({ platform: "openclaw", accountId, conversationId }),
|
|
484
|
+
);
|
|
485
|
+
if (cached?.metadataVersion != null && version <= cached.metadataVersion) {
|
|
486
|
+
log?.info?.(
|
|
487
|
+
`[${accountId}] openclaw-clawchat metadata invalidation stale conversation=${conversationId} version=${version} cached=${cached.metadataVersion}`,
|
|
488
|
+
);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
await refreshConversationDetails(conversationId, {
|
|
494
|
+
source: "metadata_invalidation",
|
|
495
|
+
...(version !== undefined ? { metadataVersion: version } : {}),
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
const upsertMessagePathConversation = (env: Envelope): void => {
|
|
499
|
+
if (!store?.upsertConversationSummary) return;
|
|
500
|
+
if (env.event !== "message.send" && env.event !== "message.reply" && env.event !== "message.done") return;
|
|
501
|
+
const conversationId = typeof env.chat_id === "string" && env.chat_id.trim()
|
|
502
|
+
? env.chat_id
|
|
503
|
+
: "";
|
|
504
|
+
if (!conversationId) return;
|
|
505
|
+
const conversationType = env.chat_type === "direct" || env.chat_type === "group"
|
|
506
|
+
? env.chat_type
|
|
507
|
+
: undefined;
|
|
508
|
+
recordConnection("conversation summary upsert", () =>
|
|
509
|
+
store.upsertConversationSummary?.({
|
|
510
|
+
platform: "openclaw",
|
|
511
|
+
accountId,
|
|
512
|
+
conversationId,
|
|
513
|
+
...(conversationType ? { conversationType } : {}),
|
|
514
|
+
lastSeenAt: env.emitted_at,
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
};
|
|
137
518
|
const client = createOpenclawClawlingClient(account, {
|
|
138
519
|
...(params.transport ? { transport: params.transport } : {}),
|
|
520
|
+
wsLifecycle: {
|
|
521
|
+
onConnectFrameSent: (env) => {
|
|
522
|
+
lastConnectTraceId = typeof env.trace_id === "string" ? env.trace_id : "-";
|
|
523
|
+
if (store && currentConnectionId != null) {
|
|
524
|
+
const connectionId = currentConnectionId;
|
|
525
|
+
recordConnection("connect-sent", () => store.markConnectSent(connectionId));
|
|
526
|
+
}
|
|
527
|
+
const deviceId =
|
|
528
|
+
typeof env.payload?.device_id === "string" ? env.payload.device_id : "-";
|
|
529
|
+
const current = wsLogContext();
|
|
530
|
+
log?.info?.(
|
|
531
|
+
formatWsLog({
|
|
532
|
+
event: "connect_sent",
|
|
533
|
+
accountId,
|
|
534
|
+
attempt: current.attempt,
|
|
535
|
+
reconnectCount: current.reconnectCount,
|
|
536
|
+
state: "handshaking",
|
|
537
|
+
action: "await_hello",
|
|
538
|
+
fields: [
|
|
539
|
+
["trace_id", lastConnectTraceId],
|
|
540
|
+
["device_id", deviceId],
|
|
541
|
+
],
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
},
|
|
139
546
|
});
|
|
547
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime client created`);
|
|
548
|
+
|
|
549
|
+
setAlignedOutboundLogContext(client, wsLogContext);
|
|
550
|
+
client.on("hello:ok", (env: Envelope) => {
|
|
551
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
552
|
+
? env.payload as { device_id?: unknown; delivery_mode?: unknown }
|
|
553
|
+
: {};
|
|
554
|
+
lastHelloOkDeviceId = typeof payload.device_id === "string" ? payload.device_id : undefined;
|
|
555
|
+
lastHelloOkDeliveryMode = typeof payload.delivery_mode === "string" ? payload.delivery_mode : undefined;
|
|
556
|
+
});
|
|
557
|
+
const protocolControlLogger = createProtocolControlHandler({
|
|
558
|
+
accountId,
|
|
559
|
+
log: (msg) => log?.info?.(msg),
|
|
560
|
+
send: () => {},
|
|
561
|
+
context: wsLogContext,
|
|
562
|
+
});
|
|
563
|
+
const logAuthFailure = (reason: string) => {
|
|
564
|
+
if (authFailureLogged) return;
|
|
565
|
+
authFailureLogged = true;
|
|
566
|
+
const current = wsLogContext();
|
|
567
|
+
log?.error?.(
|
|
568
|
+
formatWsLog({
|
|
569
|
+
event: "auth_failed",
|
|
570
|
+
accountId,
|
|
571
|
+
attempt: current.attempt,
|
|
572
|
+
reconnectCount: current.reconnectCount,
|
|
573
|
+
state: "auth_failed",
|
|
574
|
+
action: "stop_reconnect",
|
|
575
|
+
fields: [
|
|
576
|
+
["trace_id", lastHelloFailTraceId],
|
|
577
|
+
["reason", reason || lastHelloFailReason],
|
|
578
|
+
],
|
|
579
|
+
}),
|
|
580
|
+
);
|
|
581
|
+
};
|
|
582
|
+
let dispatchActivationBootstrap: () => Promise<void> = async () => {};
|
|
140
583
|
|
|
141
584
|
client.on("state", ({ from, to }) => {
|
|
142
585
|
log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
|
|
586
|
+
wsReady = to === "connected";
|
|
587
|
+
if (to === "connecting") {
|
|
588
|
+
reconnectTracker.connectStart();
|
|
589
|
+
currentAttemptStartedAt = Date.now();
|
|
590
|
+
const current = wsLogContext();
|
|
591
|
+
if (store) {
|
|
592
|
+
recordConnection("start", () => {
|
|
593
|
+
currentConnectionId = store.startConnection({
|
|
594
|
+
platform: "openclaw",
|
|
595
|
+
accountId,
|
|
596
|
+
attempt: current.attempt,
|
|
597
|
+
reconnectCount: current.reconnectCount,
|
|
598
|
+
connectStartedAt: currentAttemptStartedAt,
|
|
599
|
+
});
|
|
600
|
+
currentConnectionFinished = false;
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
log?.info?.(
|
|
604
|
+
formatWsLog({
|
|
605
|
+
event: "connect_start",
|
|
606
|
+
accountId,
|
|
607
|
+
attempt: current.attempt,
|
|
608
|
+
reconnectCount: current.reconnectCount,
|
|
609
|
+
state: "connecting",
|
|
610
|
+
action: "connect",
|
|
611
|
+
fields: [
|
|
612
|
+
["url", account.websocketUrl],
|
|
613
|
+
["queue_size", getAlignedOutboundQueueSize(client)],
|
|
614
|
+
],
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
} else if (to === "connected") {
|
|
618
|
+
const elapsedMs = Math.max(0, Date.now() - currentAttemptStartedAt);
|
|
619
|
+
const queueSize = getAlignedOutboundQueueSize(client);
|
|
620
|
+
reconnectTracker.markReady();
|
|
621
|
+
const current = wsLogContext();
|
|
622
|
+
if (store && currentConnectionId != null) {
|
|
623
|
+
const connectionId = currentConnectionId;
|
|
624
|
+
recordConnection("ready", () => {
|
|
625
|
+
if (lastHelloOkDeviceId === undefined && lastHelloOkDeliveryMode === undefined) {
|
|
626
|
+
store.markConnectionReady(connectionId);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
store.markConnectionReady(connectionId, {
|
|
630
|
+
...(lastHelloOkDeviceId !== undefined ? { resolvedDeviceId: lastHelloOkDeviceId } : {}),
|
|
631
|
+
...(lastHelloOkDeliveryMode !== undefined ? { deliveryMode: lastHelloOkDeliveryMode } : {}),
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
log?.info?.(
|
|
636
|
+
formatWsLog({
|
|
637
|
+
event: "handshake_ok",
|
|
638
|
+
accountId,
|
|
639
|
+
attempt: current.attempt,
|
|
640
|
+
reconnectCount: current.reconnectCount,
|
|
641
|
+
state: "ready",
|
|
642
|
+
action: "flush_queue",
|
|
643
|
+
fields: [
|
|
644
|
+
["trace_id", lastConnectTraceId],
|
|
645
|
+
["elapsed_ms", elapsedMs],
|
|
646
|
+
["queue_size", queueSize],
|
|
647
|
+
],
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
try {
|
|
651
|
+
flushAlignedOutboundQueue(client);
|
|
652
|
+
} catch {
|
|
653
|
+
// The queue keeps the failed frame at the head and will retry after the next reconnect.
|
|
654
|
+
}
|
|
655
|
+
void refreshConversationCacheAfterReady();
|
|
656
|
+
void dispatchActivationBootstrap();
|
|
657
|
+
} else if (to === "disconnected") {
|
|
658
|
+
reconnectTracker.markClosed();
|
|
659
|
+
}
|
|
143
660
|
const next = { ...getStatus(), ...mapClawlingStateToStatus(to as ClawlingState) };
|
|
144
661
|
setStatus(next);
|
|
145
662
|
});
|
|
146
663
|
|
|
664
|
+
client.on("close", ({ code, reason }: { code?: number; reason?: string }) => {
|
|
665
|
+
if (closingForAbort || (code === 1000 && reason === "client close")) return;
|
|
666
|
+
finishCurrentConnection({
|
|
667
|
+
state: "disconnected",
|
|
668
|
+
closeCode: code ?? null,
|
|
669
|
+
closeReason: reason ?? null,
|
|
670
|
+
});
|
|
671
|
+
const current = wsLogContext();
|
|
672
|
+
log?.info?.(
|
|
673
|
+
formatWsLog({
|
|
674
|
+
event: "connection_lost",
|
|
675
|
+
accountId,
|
|
676
|
+
attempt: current.attempt,
|
|
677
|
+
reconnectCount: current.reconnectCount,
|
|
678
|
+
state: current.state,
|
|
679
|
+
action: "reconnect",
|
|
680
|
+
fields: [
|
|
681
|
+
["code", code],
|
|
682
|
+
["reason", reason],
|
|
683
|
+
],
|
|
684
|
+
}),
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
client.on("reconnect:scheduled", ({ delay }: { delay?: number }) => {
|
|
689
|
+
reconnectTracker.scheduleReconnect("connection_lost", {
|
|
690
|
+
delayMs: delay,
|
|
691
|
+
maxDelayMs: account.reconnect.maxDelay,
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
client.on("raw", (env: Envelope) => {
|
|
696
|
+
if (env.event === "connect.challenge") {
|
|
697
|
+
const payload = env.payload as { nonce?: unknown } | undefined;
|
|
698
|
+
const current = wsLogContext();
|
|
699
|
+
log?.info?.(
|
|
700
|
+
formatWsLog({
|
|
701
|
+
event: "challenge_received",
|
|
702
|
+
accountId,
|
|
703
|
+
attempt: current.attempt,
|
|
704
|
+
reconnectCount: current.reconnectCount,
|
|
705
|
+
state: "handshaking",
|
|
706
|
+
action: "send_connect",
|
|
707
|
+
fields: [
|
|
708
|
+
["challenge_trace_id", env.trace_id],
|
|
709
|
+
["has_nonce", typeof payload?.nonce === "string" && payload.nonce.length > 0],
|
|
710
|
+
],
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (env.event === "ping" || env.event === "pong") {
|
|
715
|
+
protocolControlLogger.handleInbound(env);
|
|
716
|
+
}
|
|
717
|
+
if (wsReady) {
|
|
718
|
+
upsertMessagePathConversation(env);
|
|
719
|
+
const sender = env.sender as { id?: unknown } | undefined;
|
|
720
|
+
const senderId = typeof sender?.id === "string" ? sender.id : "-";
|
|
721
|
+
if (env.event === "message.send" || env.event === "message.reply" || env.event === "message.done") {
|
|
722
|
+
const current = wsLogContext();
|
|
723
|
+
log?.info?.(
|
|
724
|
+
formatWsLog({
|
|
725
|
+
event: "inbound_dispatch",
|
|
726
|
+
accountId,
|
|
727
|
+
attempt: current.attempt,
|
|
728
|
+
reconnectCount: current.reconnectCount,
|
|
729
|
+
state: "ready",
|
|
730
|
+
action: "dispatch",
|
|
731
|
+
fields: [
|
|
732
|
+
["event_name", env.event],
|
|
733
|
+
["trace_id", env.trace_id],
|
|
734
|
+
["chat_id", env.chat_id],
|
|
735
|
+
["sender_id", senderId],
|
|
736
|
+
],
|
|
737
|
+
}),
|
|
738
|
+
);
|
|
739
|
+
} else if (env.event === "message.ack") {
|
|
740
|
+
const current = wsLogContext();
|
|
741
|
+
log?.info?.(
|
|
742
|
+
formatWsLog({
|
|
743
|
+
event: "inbound_control",
|
|
744
|
+
accountId,
|
|
745
|
+
attempt: current.attempt,
|
|
746
|
+
reconnectCount: current.reconnectCount,
|
|
747
|
+
state: "ready",
|
|
748
|
+
action: "ack",
|
|
749
|
+
fields: [
|
|
750
|
+
["event_name", env.event],
|
|
751
|
+
["trace_id", env.trace_id],
|
|
752
|
+
],
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
755
|
+
} else if (
|
|
756
|
+
env.event === "offline.batch" ||
|
|
757
|
+
env.event === "offline.ack" ||
|
|
758
|
+
env.event === "offline.done"
|
|
759
|
+
) {
|
|
760
|
+
const current = wsLogContext();
|
|
761
|
+
log?.info?.(
|
|
762
|
+
formatWsLog({
|
|
763
|
+
event: "inbound_control",
|
|
764
|
+
accountId,
|
|
765
|
+
attempt: current.attempt,
|
|
766
|
+
reconnectCount: current.reconnectCount,
|
|
767
|
+
state: "ready",
|
|
768
|
+
action: "ignore_legacy",
|
|
769
|
+
fields: [
|
|
770
|
+
["event_name", env.event],
|
|
771
|
+
["trace_id", env.trace_id],
|
|
772
|
+
],
|
|
773
|
+
}),
|
|
774
|
+
);
|
|
775
|
+
} else if (env.event !== "ping" && env.event !== "pong") {
|
|
776
|
+
const current = wsLogContext();
|
|
777
|
+
log?.info?.(
|
|
778
|
+
formatWsLog({
|
|
779
|
+
event: "inbound_ignored",
|
|
780
|
+
accountId,
|
|
781
|
+
attempt: current.attempt,
|
|
782
|
+
reconnectCount: current.reconnectCount,
|
|
783
|
+
state: "ready",
|
|
784
|
+
action: "ignore",
|
|
785
|
+
fields: [
|
|
786
|
+
["event_name", env.event],
|
|
787
|
+
["trace_id", env.trace_id],
|
|
788
|
+
],
|
|
789
|
+
}),
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (env.event !== "hello-fail") return;
|
|
794
|
+
lastHelloFailTraceId = env.trace_id;
|
|
795
|
+
const payload = env.payload as { reason?: unknown } | undefined;
|
|
796
|
+
lastHelloFailReason = typeof payload?.reason === "string" ? payload.reason : "";
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
client.on("metadata:invalidated", (env: Envelope) => {
|
|
800
|
+
void handleMetadataInvalidation(env);
|
|
801
|
+
});
|
|
802
|
+
|
|
147
803
|
client.on("error", (err: unknown) => {
|
|
148
804
|
const classified = classifyClawlingClientError(err);
|
|
149
805
|
if (classified.kind === "auth") {
|
|
150
|
-
|
|
806
|
+
finishCurrentConnection({
|
|
807
|
+
state: "auth_failed",
|
|
808
|
+
error: lastHelloFailReason || classified.message,
|
|
809
|
+
});
|
|
810
|
+
logAuthFailure(classified.message);
|
|
151
811
|
setStatus({
|
|
152
812
|
...getStatus(),
|
|
153
813
|
connected: false,
|
|
@@ -156,6 +816,22 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
156
816
|
lastError: classified.message,
|
|
157
817
|
});
|
|
158
818
|
} else if (classified.kind === "transport") {
|
|
819
|
+
finishCurrentConnection({ state: "transport_error", error: classified.message });
|
|
820
|
+
const current = wsLogContext();
|
|
821
|
+
log?.info?.(
|
|
822
|
+
formatWsLog({
|
|
823
|
+
event: "connection_lost",
|
|
824
|
+
accountId,
|
|
825
|
+
attempt: current.attempt,
|
|
826
|
+
reconnectCount: current.reconnectCount,
|
|
827
|
+
state: current.state,
|
|
828
|
+
action: "reconnect",
|
|
829
|
+
fields: [
|
|
830
|
+
["code", "-"],
|
|
831
|
+
["reason", classified.message],
|
|
832
|
+
],
|
|
833
|
+
}),
|
|
834
|
+
);
|
|
159
835
|
log?.info?.(
|
|
160
836
|
`[${accountId}] openclaw-clawchat transport error (reconnecting): ${classified.message}`,
|
|
161
837
|
);
|
|
@@ -167,169 +843,270 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
167
843
|
} else if (classified.kind === "state") {
|
|
168
844
|
log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
|
|
169
845
|
} else {
|
|
170
|
-
log?.error?.(`[${accountId}] openclaw-clawchat
|
|
846
|
+
log?.error?.(`[${accountId}] openclaw-clawchat client error: ${classified.message}`);
|
|
171
847
|
}
|
|
172
848
|
});
|
|
173
849
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ingest: async (turn) => {
|
|
183
|
-
const rt = runtime.channel;
|
|
184
|
-
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
185
|
-
const routeCfg = withClawChatSessionScope(cfg);
|
|
186
|
-
const route = rt.routing.resolveAgentRoute({
|
|
187
|
-
cfg: routeCfg,
|
|
188
|
-
channel: CHANNEL_ID,
|
|
850
|
+
type IngestTurnResult = "submitted" | "skipped" | "failed";
|
|
851
|
+
|
|
852
|
+
const ingestTurn = async (turn: IngestTurnParams): Promise<IngestTurnResult> => {
|
|
853
|
+
const env = turn.envelope;
|
|
854
|
+
if (store?.claimMessageOnce) {
|
|
855
|
+
const claimed = recordConnection("message claim", () =>
|
|
856
|
+
store.claimMessageOnce?.({
|
|
857
|
+
platform: "openclaw",
|
|
189
858
|
accountId,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
859
|
+
kind: "message",
|
|
860
|
+
direction: "inbound",
|
|
861
|
+
eventType: String(env.event),
|
|
862
|
+
traceId: turn.traceId,
|
|
863
|
+
chatId: turn.peer.id,
|
|
864
|
+
messageId: turn.messageId,
|
|
865
|
+
text: turn.rawBody,
|
|
866
|
+
raw: env,
|
|
867
|
+
}),
|
|
868
|
+
);
|
|
869
|
+
if (claimed === false) {
|
|
870
|
+
log?.info?.(
|
|
871
|
+
`[${accountId}] openclaw-clawchat skip duplicate stored msg=${turn.messageId}`,
|
|
872
|
+
);
|
|
873
|
+
return "skipped";
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const rt = runtime.channel;
|
|
877
|
+
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
878
|
+
const routeCfg = withClawChatSessionScope(cfg);
|
|
879
|
+
const route = rt.routing.resolveAgentRoute({
|
|
880
|
+
cfg: routeCfg,
|
|
881
|
+
channel: CHANNEL_ID,
|
|
882
|
+
accountId,
|
|
883
|
+
peer: turn.peer,
|
|
884
|
+
});
|
|
885
|
+
const body = rt.reply.formatAgentEnvelope({
|
|
886
|
+
channel: "Clawling Chat",
|
|
887
|
+
from: formatConversationSubject(turn.peer),
|
|
888
|
+
body: turn.rawBody,
|
|
889
|
+
timestamp: turn.timestamp,
|
|
890
|
+
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
891
|
+
});
|
|
892
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
893
|
+
const ctxPayload = rt.turn.buildContext({
|
|
894
|
+
channel: CHANNEL_ID,
|
|
895
|
+
accountId: route.accountId ?? accountId,
|
|
896
|
+
provider: CHANNEL_ID,
|
|
897
|
+
surface: CHANNEL_ID,
|
|
898
|
+
messageId: turn.messageId,
|
|
899
|
+
messageIdFull: turn.messageId,
|
|
900
|
+
timestamp: turn.timestamp,
|
|
901
|
+
from: conversationTarget,
|
|
902
|
+
sender: {
|
|
903
|
+
id: turn.senderId,
|
|
904
|
+
name: turn.senderNickName || turn.senderId,
|
|
905
|
+
displayLabel: turn.senderNickName || turn.senderId,
|
|
906
|
+
},
|
|
907
|
+
conversation: {
|
|
908
|
+
kind: turn.peer.kind,
|
|
909
|
+
id: turn.peer.id,
|
|
910
|
+
label: formatConversationSubject(turn.peer),
|
|
911
|
+
routePeer: turn.peer,
|
|
912
|
+
},
|
|
913
|
+
route: {
|
|
914
|
+
agentId: route.agentId,
|
|
915
|
+
accountId: route.accountId ?? accountId,
|
|
916
|
+
routeSessionKey: route.sessionKey,
|
|
917
|
+
},
|
|
918
|
+
reply: {
|
|
919
|
+
to: `${CHANNEL_ID}:${account.userId}`,
|
|
920
|
+
originatingTo: conversationTarget,
|
|
921
|
+
},
|
|
922
|
+
message: {
|
|
923
|
+
body,
|
|
924
|
+
rawBody: turn.rawBody,
|
|
925
|
+
bodyForAgent: turn.rawBody,
|
|
926
|
+
commandBody: turn.rawBody,
|
|
927
|
+
envelopeFrom: conversationTarget,
|
|
928
|
+
},
|
|
929
|
+
access: {
|
|
930
|
+
mentions: {
|
|
931
|
+
canDetectMention: true,
|
|
932
|
+
wasMentioned: turn.wasMentioned,
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
...(turn.peer.kind === "group"
|
|
936
|
+
? { supplemental: { groupSystemPrompt: CLAWCHAT_GROUP_SYSTEM_PROMPT } }
|
|
937
|
+
: {}),
|
|
938
|
+
});
|
|
939
|
+
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
940
|
+
const inboundPaths =
|
|
941
|
+
turn.mediaItems.length > 0
|
|
942
|
+
? await fetchInboundMedia(turn.mediaItems, {
|
|
943
|
+
runtime,
|
|
944
|
+
log,
|
|
945
|
+
maxBytes: 20 * 1024 * 1024,
|
|
946
|
+
})
|
|
947
|
+
: [];
|
|
948
|
+
if (inboundPaths.length > 0) {
|
|
949
|
+
(ctxPayload as Record<string, unknown>).MediaPath = inboundPaths[0];
|
|
950
|
+
(ctxPayload as Record<string, unknown>).MediaPaths = inboundPaths;
|
|
951
|
+
}
|
|
236
952
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
log?.error?.(
|
|
244
|
-
`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`,
|
|
245
|
-
);
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
} catch (err) {
|
|
953
|
+
try {
|
|
954
|
+
await rt.session.recordInboundSession({
|
|
955
|
+
storePath,
|
|
956
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
957
|
+
ctx: ctxPayload,
|
|
958
|
+
onRecordError: (err) => {
|
|
249
959
|
log?.error?.(
|
|
250
960
|
`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`,
|
|
251
961
|
);
|
|
252
|
-
}
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
} catch (err) {
|
|
965
|
+
log?.error?.(
|
|
966
|
+
`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
253
969
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
970
|
+
const replyCtx = turn.replyCtx;
|
|
971
|
+
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
972
|
+
createOpenclawClawlingReplyDispatcher({
|
|
973
|
+
cfg,
|
|
974
|
+
runtime,
|
|
975
|
+
account,
|
|
976
|
+
client,
|
|
977
|
+
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
978
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
979
|
+
inboundMessageId: turn.messageId,
|
|
980
|
+
inboundForFinalReply: {
|
|
981
|
+
chatId: turn.peer.id,
|
|
982
|
+
senderId: turn.senderId,
|
|
983
|
+
senderNickName: turn.senderNickName || turn.senderId,
|
|
984
|
+
bodyText: turn.rawBody,
|
|
985
|
+
},
|
|
986
|
+
store: store
|
|
987
|
+
? {
|
|
988
|
+
insertMessage: (input) => store.insertMessage?.(input) ?? null,
|
|
989
|
+
claimMessageOnce: (input) => store.claimMessageOnce?.(input) ?? null,
|
|
990
|
+
updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
|
|
991
|
+
}
|
|
992
|
+
: null,
|
|
993
|
+
log,
|
|
994
|
+
});
|
|
272
995
|
|
|
273
|
-
|
|
996
|
+
const agentsConfigured = Object.keys((cfg as { agents?: Record<string, unknown> }).agents ?? {});
|
|
997
|
+
log?.info?.(
|
|
998
|
+
`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${ctxPayload.SessionKey ?? route.sessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`,
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
const dispatchResult = await rt.reply.withReplyDispatcher({
|
|
1003
|
+
dispatcher,
|
|
1004
|
+
onSettled: () => markDispatchIdle(),
|
|
1005
|
+
run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
|
|
1006
|
+
});
|
|
1007
|
+
const counts = (dispatchResult as { counts?: Record<string, number> } | undefined)?.counts ?? {};
|
|
1008
|
+
const queuedFinal = Boolean(
|
|
1009
|
+
(dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
|
|
1010
|
+
);
|
|
1011
|
+
log?.info?.(
|
|
1012
|
+
`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
|
|
1013
|
+
);
|
|
1014
|
+
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
274
1015
|
log?.info?.(
|
|
275
|
-
`[${accountId}] openclaw-clawchat
|
|
1016
|
+
`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
|
|
1017
|
+
`Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
|
|
1018
|
+
`or send-policy denied; or a plugin claimed the binding.`,
|
|
276
1019
|
);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
log?.error?.(
|
|
301
|
-
`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`,
|
|
302
|
-
);
|
|
303
|
-
await sendOpenclawClawlingText({
|
|
304
|
-
client,
|
|
305
|
-
account: turn.account,
|
|
306
|
-
to: {
|
|
307
|
-
chatId: turn.peer.id,
|
|
308
|
-
chatType: turn.peer.kind === "group" ? "group" : "direct",
|
|
309
|
-
},
|
|
310
|
-
text: String(err),
|
|
311
|
-
...(turn.replyCtx ? { replyCtx: turn.replyCtx } : {}),
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
})
|
|
1020
|
+
}
|
|
1021
|
+
return "submitted";
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
log?.error?.(
|
|
1024
|
+
`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`,
|
|
1025
|
+
);
|
|
1026
|
+
return "failed";
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const handleInboundEnvelope = async (env: Envelope): Promise<IngestTurnResult | undefined> => {
|
|
1031
|
+
let ingestResult: IngestTurnResult | undefined;
|
|
1032
|
+
try {
|
|
1033
|
+
await dispatchOpenclawClawlingInbound({
|
|
1034
|
+
envelope: env as Envelope<unknown>,
|
|
1035
|
+
cfg,
|
|
1036
|
+
runtime,
|
|
1037
|
+
account,
|
|
1038
|
+
log,
|
|
1039
|
+
ingest: async (turn) => {
|
|
1040
|
+
ingestResult = await ingestTurn(turn);
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
316
1043
|
} catch (err) {
|
|
317
1044
|
log?.error?.(
|
|
318
1045
|
`[${accountId}] openclaw-clawchat message handler error: ${err instanceof Error ? err.stack || err.message : String(err)}`,
|
|
319
1046
|
);
|
|
1047
|
+
return "failed";
|
|
320
1048
|
}
|
|
1049
|
+
return ingestResult;
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
dispatchActivationBootstrap = async (): Promise<void> => {
|
|
1053
|
+
if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent) return;
|
|
1054
|
+
let bootstrap: { conversationId: string } | null | undefined;
|
|
1055
|
+
const releaseBootstrap = () => {
|
|
1056
|
+
if (!bootstrap || !store.releaseActivationBootstrapClaim) return;
|
|
1057
|
+
const claimedBootstrap = bootstrap;
|
|
1058
|
+
recordConnection("activation bootstrap release", () =>
|
|
1059
|
+
store.releaseActivationBootstrapClaim?.({
|
|
1060
|
+
platform: "openclaw",
|
|
1061
|
+
accountId,
|
|
1062
|
+
conversationId: claimedBootstrap.conversationId,
|
|
1063
|
+
}),
|
|
1064
|
+
);
|
|
1065
|
+
};
|
|
1066
|
+
try {
|
|
1067
|
+
bootstrap = recordConnection("activation bootstrap claim", () =>
|
|
1068
|
+
store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }),
|
|
1069
|
+
);
|
|
1070
|
+
if (!bootstrap) return;
|
|
1071
|
+
const claimedBootstrap = bootstrap;
|
|
1072
|
+
const result = await handleInboundEnvelope(
|
|
1073
|
+
buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
|
|
1074
|
+
);
|
|
1075
|
+
if (result !== "submitted") {
|
|
1076
|
+
releaseBootstrap();
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
recordConnection("activation bootstrap sent", () =>
|
|
1080
|
+
store.markActivationBootstrapSent?.({
|
|
1081
|
+
platform: "openclaw",
|
|
1082
|
+
accountId,
|
|
1083
|
+
conversationId: claimedBootstrap.conversationId,
|
|
1084
|
+
}),
|
|
1085
|
+
);
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
releaseBootstrap();
|
|
1088
|
+
log?.error?.(
|
|
1089
|
+
`[${accountId}] openclaw-clawchat activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
client.on("message", (env: Envelope) => {
|
|
1095
|
+
void handleInboundEnvelope(env);
|
|
321
1096
|
});
|
|
322
1097
|
|
|
323
1098
|
// `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
|
|
324
1099
|
// (auth). Transport failures (server unreachable, DNS error, etc.) do
|
|
325
|
-
// NOT reject this promise — the
|
|
326
|
-
// its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
1100
|
+
// NOT reject this promise — the local client handles them internally and
|
|
1101
|
+
// drives its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
327
1102
|
// capped at `maxDelay`, with jitter). So we never throw here on anything
|
|
328
1103
|
// other than auth failure; on auth we tear the account down cleanly and
|
|
329
1104
|
// return without throwing (which would make the gateway supervisor
|
|
330
1105
|
// restart us immediately in a tight loop).
|
|
331
1106
|
try {
|
|
1107
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime calling client.connect()`);
|
|
332
1108
|
await client.connect();
|
|
1109
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime client.connect() resolved`);
|
|
333
1110
|
} catch (err) {
|
|
334
1111
|
const classified = classifyClawlingClientError(err);
|
|
335
1112
|
setStatus({
|
|
@@ -339,12 +1116,21 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
339
1116
|
running: false,
|
|
340
1117
|
lastError: classified.message,
|
|
341
1118
|
});
|
|
1119
|
+
if (classified.kind === "auth") {
|
|
1120
|
+
finishCurrentConnection({
|
|
1121
|
+
state: "auth_failed",
|
|
1122
|
+
error: lastHelloFailReason || classified.message,
|
|
1123
|
+
});
|
|
1124
|
+
logAuthFailure(classified.message);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
342
1127
|
log?.error?.(
|
|
343
1128
|
`[${accountId}] openclaw-clawchat connect failed (${classified.kind}): ${classified.message}`,
|
|
344
1129
|
);
|
|
345
1130
|
return;
|
|
346
1131
|
}
|
|
347
1132
|
activeClients.set(accountId, client);
|
|
1133
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime active client registered`);
|
|
348
1134
|
setStatus({
|
|
349
1135
|
...getStatus(),
|
|
350
1136
|
connected: true,
|
|
@@ -354,7 +1140,14 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
354
1140
|
log?.info?.(`[${accountId}] openclaw-clawchat connected`);
|
|
355
1141
|
|
|
356
1142
|
await waitUntilAbort(abortSignal, async () => {
|
|
1143
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime abort received; closing client`);
|
|
357
1144
|
activeClients.delete(accountId);
|
|
1145
|
+
closingForAbort = true;
|
|
1146
|
+
finishCurrentConnection({
|
|
1147
|
+
state: "disconnected",
|
|
1148
|
+
closeCode: 1000,
|
|
1149
|
+
closeReason: "client close",
|
|
1150
|
+
});
|
|
358
1151
|
client.close();
|
|
359
1152
|
setStatus({
|
|
360
1153
|
...getStatus(),
|